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" );