diff --git a/src/TcBlack/BuildTwinCatProject.bat b/src/TcBlack/BuildTwinCatProject.bat
new file mode 100644
index 0000000..44bdfd7
--- /dev/null
+++ b/src/TcBlack/BuildTwinCatProject.bat
@@ -0,0 +1 @@
+start /wait "" %1 %2 /Project %3 /Build "Debug|TwinCAT RT (x64)" /Out build.log
\ No newline at end of file
diff --git a/src/TcBlack/MessageFilter.cs b/src/TcBlack/MessageFilter.cs
deleted file mode 100644
index e9bf94d..0000000
--- a/src/TcBlack/MessageFilter.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace TcBlack
-{
- enum ServiceCall
- {
- TooBusyCancelAll = -1,
- IsHandled = 0,
- PendingMsg_WaitDefProcess = 2,
- RetryLater = 99,
- }
-
- ///
- /// Prevents threading contention issues between external multi-threaded
- /// applications and Visual Studio.
- ///
- ///
- public class MessageFilter : IOleMessageFilter
- {
- ///
- /// Start the filter.
- ///
- public static void Register()
- {
- IOleMessageFilter newFilter = new MessageFilter();
- CoRegisterMessageFilter(newFilter, out IOleMessageFilter oldFilter);
- }
-
- ///
- /// Done with the filter, close it.
- ///
- public static void Revoke()
- {
- CoRegisterMessageFilter(null, out IOleMessageFilter oldFilter);
- }
-
- ///
- /// Handle incoming thread requests.
- ///
- ///
- ///
- ///
- ///
- ///
- int IOleMessageFilter.HandleInComingCall(
- int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo
- )
- {
- return (int)ServiceCall.IsHandled;
- }
-
- ///
- /// Thread call was rejected, so try again.
- ///
- ///
- ///
- ///
- ///
- int IOleMessageFilter.RetryRejectedCall(
- IntPtr hTaskCallee, int dwTickCount, int dwRejectType
- )
- {
- if (dwRejectType == 2)
- {
- // Retry the thread call immediately if return >=0 & <100.
- return (int)ServiceCall.RetryLater;
- }
- return (int)ServiceCall.TooBusyCancelAll;
- }
-
- int IOleMessageFilter.MessagePending(
- IntPtr hTaskCallee, int dwTickCount, int dwPendingType
- )
- {
- return (int)ServiceCall.PendingMsg_WaitDefProcess;
- }
-
- ///
- /// Implement the IOleMessageFilter interface.
- ///
- ///
- ///
- ///
- [DllImport("Ole32.dll")]
- private static extern int CoRegisterMessageFilter(
- IOleMessageFilter newFilter, out IOleMessageFilter oldFilter
- );
- }
-
- [
- ComImport(),
- Guid("00000016-0000-0000-C000-000000000046"),
- InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
- ]
- interface IOleMessageFilter
- {
- [PreserveSig]
- int HandleInComingCall(
- int dwCallType,
- IntPtr hTaskCaller,
- int dwTickCount,
- IntPtr lpInterfaceInfo
- );
-
- [PreserveSig]
- int RetryRejectedCall(
- IntPtr hTaskCallee,
- int dwTickCount,
- int dwRejectType
- );
-
- [PreserveSig]
- int MessagePending(
- IntPtr hTaskCallee,
- int dwTickCount,
- int dwPendingType
- );
- }
-}
diff --git a/src/TcBlack/NLog.config b/src/TcBlack/NLog.config
deleted file mode 100644
index 2611215..0000000
--- a/src/TcBlack/NLog.config
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/TcBlack/Program.cs b/src/TcBlack/Program.cs
index 1d3c559..6271fcc 100644
--- a/src/TcBlack/Program.cs
+++ b/src/TcBlack/Program.cs
@@ -52,9 +52,14 @@ class Options
HelpText = "Overrides the line ending of all files with UNIX' \\n."
)]
public bool UnixLineEnding { get; set; }
+
+ [Option(
+ Default = false,
+ HelpText = "Outputs build info. Has no effect in non-safe mode."
+ )]
+ public bool Verbose { get; set; }
}
- [STAThread]
static void Main(string[] args)
{
Parser.Default.ParseArguments(args).WithParsed(options =>
@@ -150,7 +155,7 @@ static void SafeFormat(string[] filenames, Options options)
string hashBeforeFormat = string.Empty;
try
{
- hashBeforeFormat = tcProject.Build().Hash;
+ hashBeforeFormat = tcProject.Build(options.Verbose).Hash;
}
catch(ProjectBuildFailed)
{
@@ -179,7 +184,7 @@ static void SafeFormat(string[] filenames, Options options)
string hashAfterFormat = string.Empty;
try
{
- hashAfterFormat = tcProject.Build().Hash;
+ hashAfterFormat = tcProject.Build(options.Verbose).Hash;
}
catch(ProjectBuildFailed)
{
diff --git a/src/TcBlack/TcBlack.csproj b/src/TcBlack/TcBlack.csproj
index 562c76a..d0c74b9 100644
--- a/src/TcBlack/TcBlack.csproj
+++ b/src/TcBlack/TcBlack.csproj
@@ -35,16 +35,8 @@
..\packages\CommandLineParser.2.8.0\lib\net461\CommandLine.dll
-
- ..\packages\NLog.4.7.5\lib\net45\NLog.dll
-
-
-
-
-
-
@@ -58,7 +50,6 @@
-
@@ -70,12 +61,10 @@
-
-
- Designer
+
PreserveNewest
@@ -83,24 +72,6 @@
-
- {80CC9F66-E7D8-4DDD-85B6-D9E6CD0E93E2}
- 8
- 0
- 0
- tlbimp
- False
- True
-
-
- {1A31287A-4D7D-413E-8E32-3B374931BD89}
- 8
- 0
- 0
- tlbimp
- False
- True
-
{00020430-0000-0000-C000-000000000046}
2
@@ -110,15 +81,6 @@
False
True
-
- {3C49D6C3-93DC-11D0-B162-00A0248C244B}
- 3
- 3
- 0
- primary
- False
- True
-
\ No newline at end of file
diff --git a/src/TcBlack/TcProjectBuilder.cs b/src/TcBlack/TcProjectBuilder.cs
index 6c29118..1dc2d8d 100644
--- a/src/TcBlack/TcProjectBuilder.cs
+++ b/src/TcBlack/TcProjectBuilder.cs
@@ -1,5 +1,7 @@
-using System;
+using Microsoft.Win32;
+using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
@@ -18,60 +20,17 @@ public ProjectBuildFailed()
///
public class TcProjectBuilder
{
- private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
- private readonly string slnPath;
private readonly string projectPath;
- private readonly string tcVersion;
- private static VisualStudioInstance vsInstance = null;
+ private readonly string slnPath;
+ private readonly string devenvPath;
+ protected string buildLogFile = "build.log";
public TcProjectBuilder(string projectOrTcPouPath)
{
- tcVersion = GetTwinCatVersionFromTsprojFile(projectOrTcPouPath);
projectPath = GetParentPath(projectOrTcPouPath, ".plcproj");
slnPath = GetParentPath(projectOrTcPouPath, ".sln");
- }
-
- ///
- /// Tries to get the version number.
- ///
- ///
- ///
- private string GetTwinCatVersionFromTsprojFile(string projectOrTcPouPath)
- {
- string version = "";
- try
- {
- string tsprojPath = GetTsprojPath(projectOrTcPouPath);
- version = GetTwinCatVersion(tsprojPath);
- }
- catch (FileNotFoundException)
- {
- }
-
- return version;
- }
-
- ///
- /// Return the path to the *.tsp(p)roj file.
- ///
- /// Path to start the search from.
- /// Path to the *.tsp(p)roj file or FileNotFoundException.
- private string GetTsprojPath(string projectOrTcPouPath)
- {
- string tsprojPath = "";
- string[] tsprojExtensions = new string[] { ".tsproj", ".tspproj" };
- foreach (string tsprojExtension in tsprojExtensions)
- {
- try
- {
- tsprojPath = GetParentPath(projectOrTcPouPath, tsprojExtension);
- }
- catch(FileNotFoundException)
- {
- }
- }
-
- return tsprojPath;
+ string vsVersion = GetVsVersion(slnPath);
+ devenvPath = GetDevEnvPath(vsVersion);
}
///
@@ -125,83 +84,110 @@ ex is DirectoryNotFoundException || ex is ArgumentException
}
///
- /// Return the TwinCAT version from the tsproj file.
+ /// Return the Visual Studio version from the solution file.
///
- /// Path the tsproj file.
- /// Version number of TwinCAT.
- private string GetTwinCatVersion(string tsprojPath)
+ /// Path the solution file.
+ /// Major and minor version number of Visual Studio.
+ private string GetVsVersion(string slnPath)
{
string file;
try
{
- file = File.ReadAllText(tsprojPath);
+ file = File.ReadAllText(slnPath);
}
catch (ArgumentException)
{
return "";
}
- string pattern = "TcVersion=\"(\\d\\.\\d\\.\\d{4}\\.\\d+)\"";
+ string pattern = @"^VisualStudioVersion\s+=\s+(?\d+\.\d+)";
Match match = Regex.Match(
file, pattern, RegexOptions.Multiline
);
- return match.Success ? match.Groups[1].Value : "";
+ if (match.Success)
+ {
+ return match.Groups[1].Value;
+ }
+ else
+ {
+ return "";
+ }
}
///
- /// Build the project file.
+ /// Return the path to devenv.com of the given Visual Studio version.
///
- public TcProjectBuilder Build()
+ ///
+ /// Visual Studio version to get the devenv.com path of.
+ ///
+ ///
+ /// The path to devenv.com of the given Visual Studio version.
+ ///
+ private string GetDevEnvPath(string vsVersion)
{
- TryLoadSolution();
- TryBuildTwinCatProject();
+ RegistryKey rkey = Registry.LocalMachine
+ .OpenSubKey(
+ @"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\SxS\VS7", false
+ );
- return this;
+ try
+ {
+ return Path.Combine(
+ rkey.GetValue(vsVersion).ToString(),
+ "Common7",
+ "IDE",
+ "devenv.com"
+ );
+ }
+ catch (NullReferenceException)
+ {
+ return "";
+ }
}
///
- ///
+ /// Build the project file.
///
- private void TryLoadSolution()
+ public TcProjectBuilder Build(bool verbose)
{
- Logger.Info("Starting solution...");
- try
- {
- MessageFilter.Register();
- vsInstance = new VisualStudioInstance(slnPath);
- vsInstance.Load(tcVersion);
- }
- catch
+ string currentDirectory = Directory.GetCurrentDirectory();
+ string buildScript = Path.Combine(
+ currentDirectory, "BuildTwinCatProject.bat"
+ );
+
+ ExecuteCommand(
+ $"{buildScript} \"{devenvPath}\" \"{slnPath}\" \"{projectPath}\"",
+ verbose
+ );
+
+ string buildLog = File.ReadAllText(buildLogFile);
+ if (BuildFailed(buildLog))
{
- // Detailed error messages output by vsInstance.Load()
- Logger.Error("Solution load failed");
- CleanUp();
throw new ProjectBuildFailed();
}
- }
- private void TryBuildTwinCatProject()
- {
- Logger.Info("Building TwinCAT project...");
- vsInstance.BuildProject(projectPath);
+ return this;
}
///
- /// Cleans the system resources (the VS DTE)
+ /// Reads the last line from the build.log file to see if the build failed.
///
- private static void CleanUp()
+ /// The
+ public bool BuildFailed(string buildLog)
{
- try
- {
- vsInstance.Close();
- }
- catch
+ string pattern =
+ @"(?:========== Build: )(\d+)(?:[a-z \-,]*)(\d+)(?:[a-z \-,]*)";
+ MatchCollection matches = Regex.Matches(buildLog, pattern);
+ if (matches.Count > 0)
{
+ var lastMatch = matches[matches.Count - 1];
+ int buildsFailed = Convert.ToInt16(lastMatch.Groups[2].Value);
+
+ return buildsFailed != 0;
}
- Logger.Info("Exiting application...");
- MessageFilter.Revoke();
+ return false;
}
///
@@ -234,5 +220,41 @@ public string Hash {
}
}
}
+
+ ///
+ /// Execute a command in the windown command prompt cmd.exe.
+ /// Source: https://stackoverflow.com/a/5519517/6329629
+ ///
+ ///
+ protected virtual void ExecuteCommand(string command, bool verbose)
+ {
+ var processInfo = new ProcessStartInfo("cmd.exe", "/c " + command)
+ {
+ CreateNoWindow = false,
+ UseShellExecute = false,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ };
+
+ var process = Process.Start(processInfo);
+
+ if (verbose)
+ {
+ process.OutputDataReceived += (object sender, DataReceivedEventArgs e)
+ => Console.WriteLine("output >> " + e.Data);
+ process.BeginOutputReadLine();
+
+ process.ErrorDataReceived += (object sender, DataReceivedEventArgs e)
+ => Console.WriteLine("error >> " + e.Data);
+ process.BeginErrorReadLine();
+ }
+
+ process.WaitForExit();
+ if (verbose)
+ {
+ Console.WriteLine("ExitCode: {0}", process.ExitCode);
+ }
+ process.Close();
+ }
}
}
diff --git a/src/TcBlack/VisualStudioInstance.cs b/src/TcBlack/VisualStudioInstance.cs
deleted file mode 100644
index 66cfab8..0000000
--- a/src/TcBlack/VisualStudioInstance.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using EnvDTE;
-using EnvDTE80;
-using NLog;
-using System;
-using System.IO;
-using System.Text.RegularExpressions;
-using TCatSysManagerLib;
-
-namespace TcBlack
-{
- ///
- /// This class is used to instantiate the Visual Studio Development Tools
- /// Environment (DTE) which is used to programatically access all the functions
- /// in VS.
- ///
- /// Source: https://github.com/tcunit/TcUnit
- class VisualStudioInstance
- {
- private DTE2 developmentToolsEnvironment;
- private bool loaded;
- private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- private readonly string solutionPath;
- private Type type;
- private Solution visualStudioSolution;
- private readonly string visualStudioVersion;
-
- public VisualStudioInstance(string visualStudioSolutionFilePath)
- {
- solutionPath = visualStudioSolutionFilePath;
- visualStudioVersion = FindVisualStudioVersion();
- }
-
- ///
- /// Loads the development tools environment
- ///
- public void Load(string twincatVersion)
- {
- loaded = true;
-
- try
- {
- LoadDevelopmentToolsEnvironment(visualStudioVersion, twincatVersion);
- }
- catch (Exception e)
- {
- string message = string.Format(
- $"{e.Message} Error loading VS DTE version {visualStudioVersion}. "
- + $"Is the correct version of Visual Studio installed?"
- );
- logger.Error(message);
- throw;
- }
-
- if (!string.IsNullOrEmpty(solutionPath))
- {
- try
- {
- LoadSolution(solutionPath);
- }
- catch (Exception e)
- {
- string message = string.Format(
- $"{e.Message} Error loading solution at \"{solutionPath}\". "
- + $"Is the path correct?"
- );
- logger.Error(message);
- throw;
- }
- }
- }
-
- ///
- /// Build a TwinCAT project.
- ///
- /// Path to plcproj file.
- public void BuildProject(string projectName)
- {
- visualStudioSolution.SolutionBuild.BuildProject(
- "Debug", projectName, true
- );
-
- if (visualStudioSolution.SolutionBuild.LastBuildInfo > 0)
- {
- throw new ProjectBuildFailed();
- }
- }
-
- ///
- /// Closes the DTE and makes sure the VS process is completely shutdown
- ///
- public void Close()
- {
- if (loaded)
- {
- logger.Info(
- "Closing the Visual Studio Development Tools Environment (DTE), "
- + "please wait..."
- );
- // Makes sure that there are no visual studio processes left in the
- // system if the user interrupts this program (for example by CTRL+C)
- //System.Threading.Thread.Sleep(20000);
- developmentToolsEnvironment.Quit();
- }
- loaded = false;
- }
-
- ///
- /// Opens the main *.sln-file and finds the version of VS used for creation of
- /// the solution
- ///
- /// The version of Visual Studio used to create the solution
- private string FindVisualStudioVersion()
- {
- string file;
- try
- {
- file = File.ReadAllText(solutionPath);
- }
- catch (ArgumentException)
- {
- return null;
- }
-
- string pattern = @"^VisualStudioVersion\s+=\s+(?\d+\.\d+)";
- Match match = Regex.Match(file, pattern, RegexOptions.Multiline);
-
- if (match.Success)
- {
- logger.Info(
- $"Found visual studio version {match.Groups[1].Value} in solution file."
- );
- return match.Groups[1].Value;
- }
- else
- {
- return null;
- }
- }
-
- private void LoadDevelopmentToolsEnvironment(
- string visualStudioVersion, string remoteManagerVersion
- )
- {
- // Make sure the DTE loads with the same version of Visual Studio as the
- // TwinCAT project was created in
- string VisualStudioProgId = "VisualStudio.DTE." + visualStudioVersion;
- type = Type.GetTypeFromProgID(VisualStudioProgId);
- logger.Info(
- "Loading the Visual Studio Development Tools Environment (DTE)..."
- );
- // have devenv.exe automatically close when launched using automation
- developmentToolsEnvironment = (DTE2)Activator.CreateInstance(type, true);
- developmentToolsEnvironment.UserControl = false;
- developmentToolsEnvironment.SuppressUI = true;
- developmentToolsEnvironment.ToolWindows.ErrorList.ShowErrors = true;
- developmentToolsEnvironment.ToolWindows.ErrorList.ShowMessages = true;
- developmentToolsEnvironment.ToolWindows.ErrorList.ShowWarnings = true;
- logger.Debug("Getting Tc automation settings");
- var tcAutomationSettings =
- developmentToolsEnvironment.GetObject("TcAutomationSettings");
- tcAutomationSettings.SilentMode = true;
- // Uncomment this if you want to run a specific version of TwinCAT
- logger.Debug($"Setting remote manager version to {remoteManagerVersion}");
- ITcRemoteManager remoteManager =
- developmentToolsEnvironment.GetObject("TcRemoteManager");
- remoteManager.Version = remoteManagerVersion;
- }
-
- private void LoadSolution(string filePath)
- {
- logger.Debug($"Loading solution {filePath}");
- visualStudioSolution = developmentToolsEnvironment.Solution;
- visualStudioSolution.Open(filePath);
- }
- }
-}
diff --git a/src/TcBlack/packages.config b/src/TcBlack/packages.config
index 5ecb0d7..d4e14b9 100644
--- a/src/TcBlack/packages.config
+++ b/src/TcBlack/packages.config
@@ -2,5 +2,4 @@
-
\ No newline at end of file
diff --git a/src/TcBlackTests/MockTcProjectBuilder.cs b/src/TcBlackTests/MockTcProjectBuilder.cs
new file mode 100644
index 0000000..df6a1f1
--- /dev/null
+++ b/src/TcBlackTests/MockTcProjectBuilder.cs
@@ -0,0 +1,24 @@
+using TcBlack;
+
+namespace TcBlackTests
+{
+ public class MockTcProjectBuilder : TcProjectBuilder
+ {
+ public MockTcProjectBuilder(
+ string projectPath, string buildLogPath
+ ) : base(projectPath)
+ {
+ buildLogFile = buildLogPath;
+ }
+
+ ///
+ /// Doesn't run cmd.exe in the mock implementation.
+ ///
+ ///
+ /// This argument doesn't have an effect in the mock implementation
+ ///
+ protected override void ExecuteCommand(string command, bool verbose)
+ {
+ }
+ }
+}
diff --git a/src/TcBlackTests/TcBlackTests.csproj b/src/TcBlackTests/TcBlackTests.csproj
index 08d66b4..4a2122b 100644
--- a/src/TcBlackTests/TcBlackTests.csproj
+++ b/src/TcBlackTests/TcBlackTests.csproj
@@ -61,6 +61,7 @@
+
diff --git a/src/TcBlackTests/TcProjectBuilderTests.cs b/src/TcBlackTests/TcProjectBuilderTests.cs
index 1cd53fc..bbe57b9 100644
--- a/src/TcBlackTests/TcProjectBuilderTests.cs
+++ b/src/TcBlackTests/TcProjectBuilderTests.cs
@@ -31,7 +31,24 @@ public void InitializeWithNonExistingPathRaiseException()
);
}
- //// Uncomment this if you want to test the real failing build process.
+ [Fact]
+ public void BuildMockBrokenProjectShouldRaiseException()
+ {
+ string brokenProjectPath = Path.Combine(
+ projectDirectory, "BrokenProjectForUnitTests", "PLC2", "PLC2.plcproj"
+ );
+ string failedBuildLogPath = Path.Combine(
+ testDirectory,
+ "TcProjectBuildTestData",
+ "failedBuildWithExtraTextBelow.log"
+ );
+ var plcProject = new MockTcProjectBuilder(
+ brokenProjectPath, failedBuildLogPath
+ );
+ Assert.Throws(() => plcProject.Build(verbose:true));
+ }
+
+ //// Only uncomment this if you want to test the real build process.
//// Takes ~30 s to complete.
//[Fact]
//public void BuildRealBrokenProjectShouldRaiseException()
@@ -40,30 +57,7 @@ public void InitializeWithNonExistingPathRaiseException()
// projectDirectory, "BrokenProjectForUnitTests", "PLC2", "PLC2.plcproj"
// );
// var plcProject = new TcProjectBuilder(brokenPlcProjectPath);
- // Assert.Throws(() => plcProject.Build());
- //}
-
- //// Uncomment this if you want to test the real successfull build process.
- //// Takes ~30 s to complete.
- //[Fact]
- //public void BuildRealWorkingProjectShouldMakeNewCompiledFile()
- //{
- // string workingPlcProjectPath = Path.Combine(
- // projectDirectory, "WorkingProjectForUnitTests", "PLC", "PLC.plcproj"
- // );
- // var plcProject = new TcProjectBuilder(workingPlcProjectPath);
- // var hash = plcProject.Build().Hash;
- // string workingProjectDirectory = Path.GetDirectoryName(
- // workingPlcProjectPath
- // );
- // var compileDate = File.GetLastWriteTime(Path.Combine(
- // workingProjectDirectory, "_CompileInfo", $"{hash}.compileinfo"
- // ));
- // Assert.Equal(
- // compileDate,
- // DateTime.Now,
- // new TimeSpan(hours: 0, minutes: 1, seconds: 0)
- // );
+ // Assert.Throws(() => plcProject.Build(verbose: true));
//}
[Theory]
@@ -76,6 +70,27 @@ public void TryGetHashOfNonExistingProject(string projectPath)
);
}
+ private static readonly string testDataDirectory = Path.Combine(
+ testDirectory, "TcProjectBuildTestData"
+ );
+ private static readonly string workingPlcProjectPath = Path.Combine(
+ projectDirectory, "WorkingProjectForUnitTests", "PLC", "PLC.plcproj"
+ );
+ [Theory]
+ [InlineData("succesfulBuild.log", false)]
+ [InlineData("failedBuildWithExtraTextBelow.log", true)]
+ [InlineData("firstBuildOkSecondBuildFailed.log", true)]
+ public void CheckIfBuildFailedFromLogFile(string logFile, bool buildFailed)
+ {
+ TcProjectBuilder tcProject = new TcProjectBuilder(workingPlcProjectPath);
+ string logFileContent = File.ReadAllText(
+ Path.Combine(testDataDirectory, logFile)
+ );
+ bool actual = tcProject.BuildFailed(logFileContent);
+
+ Assert.Equal(buildFailed, actual);
+ }
+
private static readonly string workingProjectPouDirectory = Path.Combine(
projectDirectory, "WorkingProjectForUnitTests", "PLC", "POUs"
);