From 49df1746174cbf5791f92423540de9b3d79eb072 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 29 Dec 2009 12:15:39 +0100 Subject: [PATCH] Implement process priority property on exec tasks --- UnitTests/Core/Tasks/ExecutableTaskTest.cs | 4 + UnitTests/Core/Tasks/MsBuildTaskTest.cs | 7 +- UnitTests/Core/Util/ProcessExecutorTest.cs | 8 +- UnitTests/Core/Util/ProcessInfoTest.cs | 2 +- core/sourcecontrol/RobocopySourceControl.cs | 287 ++++---- core/tasks/BaseExecutableTask.cs | 4 +- core/tasks/DevenvTask.cs | 12 +- core/tasks/DupFinderTask.cs | 1068 ++++++++++++++------------- core/tasks/ExecutableTask.cs | 16 +- core/tasks/GendarmeTask.cs | 15 + core/tasks/MsBuildTask.cs | 24 +- core/tasks/NAntTask.cs | 17 + core/tasks/NCoverProfileTask.cs | 23 +- core/tasks/NCoverReportTask.cs | 23 +- core/tasks/NDependTask.cs | 26 +- core/tasks/NUnitTask.cs | 15 +- core/tasks/PowerShellTask.cs | 707 +++++++++--------- core/tasks/RakeTask.cs | 15 + core/util/ProcessExecutor.cs | 26 +- core/util/ProcessInfo.cs | 31 +- 20 files changed, 1285 insertions(+), 1045 deletions(-) diff --git a/UnitTests/Core/Tasks/ExecutableTaskTest.cs b/UnitTests/Core/Tasks/ExecutableTaskTest.cs index 056791a..9d19fb5 100644 --- a/UnitTests/Core/Tasks/ExecutableTaskTest.cs +++ b/UnitTests/Core/Tasks/ExecutableTaskTest.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.IO; using Exortech.NetReflector; using NMock.Constraints; @@ -42,6 +43,7 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks name2 value3 + BelowNormal 0,1,3,5 "; @@ -58,6 +60,7 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks Assert.AreEqual("name3", task.EnvironmentVariables[2].name, "Checking name3 environment variable."); Assert.AreEqual("value3", task.EnvironmentVariables[2].value, "Checking name3 environment value."); Assert.AreEqual("0,1,3,5", task.SuccessExitCodes); + Assert.AreEqual(ProcessPriorityClass.BelowNormal, task.Priority); Verify(); } @@ -75,6 +78,7 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks Assert.AreEqual("", task.BuildArgs, "Checking BuildArgs property."); Assert.AreEqual(0, task.EnvironmentVariables.Length, "Checking environment variable array size."); Assert.AreEqual("", task.SuccessExitCodes); + Assert.AreEqual(ProcessPriorityClass.Normal, task.Priority); Verify(); } diff --git a/UnitTests/Core/Tasks/MsBuildTaskTest.cs b/UnitTests/Core/Tasks/MsBuildTaskTest.cs index 797e697..3d93906 100644 --- a/UnitTests/Core/Tasks/MsBuildTaskTest.cs +++ b/UnitTests/Core/Tasks/MsBuildTaskTest.cs @@ -1,12 +1,13 @@ +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Exortech.NetReflector; using NUnit.Framework; +using Rhino.Mocks; using ThoughtWorks.CruiseControl.Core; using ThoughtWorks.CruiseControl.Core.Tasks; using ThoughtWorks.CruiseControl.Core.Util; using ThoughtWorks.CruiseControl.Remote; -using Rhino.Mocks; namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks { @@ -178,6 +179,7 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks Build;Test 15 Kobush.Build.Logging.XmlLogger,Kobush.MSBuild.dll;buildresult.xml + BelowNormal "; task = (MsBuildTask) NetReflector.Read(xml); Assert.AreEqual(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50215\MSBuild.exe", task.Executable); @@ -186,7 +188,8 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Tasks Assert.AreEqual("Build;Test", task.Targets); Assert.AreEqual("/p:Configuration=Debug /v:diag", task.BuildArgs); Assert.AreEqual(15, task.Timeout); - Assert.AreEqual("Kobush.Build.Logging.XmlLogger,Kobush.MSBuild.dll;buildresult.xml", task.Logger); + Assert.AreEqual("Kobush.Build.Logging.XmlLogger,Kobush.MSBuild.dll;buildresult.xml", task.Logger); + Assert.AreEqual(ProcessPriorityClass.BelowNormal, task.Priority); } [Test] diff --git a/UnitTests/Core/Util/ProcessExecutorTest.cs b/UnitTests/Core/Util/ProcessExecutorTest.cs index b9f57fb..d9f954a 100644 --- a/UnitTests/Core/Util/ProcessExecutorTest.cs +++ b/UnitTests/Core/Util/ProcessExecutorTest.cs @@ -184,28 +184,28 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Util { int[] successExitCodes = { 1, 3, 5 }; - ProcessInfo processInfo1 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 1", null, successExitCodes); + ProcessInfo processInfo1 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 1", null, ProcessPriorityClass.AboveNormal, successExitCodes); ProcessResult result1 = executor.Execute(processInfo1); Assert.AreEqual("Hello World", result1.StandardOutput.Trim()); Assert.AreEqual(1, result1.ExitCode, "Process did not exit successfully"); AssertFalse("process should not return an error", result1.Failed); - ProcessInfo processInfo2 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 3", null, successExitCodes); + ProcessInfo processInfo2 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 3", null, ProcessPriorityClass.AboveNormal, successExitCodes); ProcessResult result2 = executor.Execute(processInfo2); Assert.AreEqual("Hello World", result2.StandardOutput.Trim()); Assert.AreEqual(3, result2.ExitCode, "Process did not exit successfully"); AssertFalse("process should not return an error", result2.Failed); - ProcessInfo processInfo3 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 5", null, successExitCodes); + ProcessInfo processInfo3 = new ProcessInfo("cmd.exe", "/C @echo Hello World & exit 5", null, ProcessPriorityClass.AboveNormal, successExitCodes); ProcessResult result3 = executor.Execute(processInfo3); Assert.AreEqual("Hello World", result3.StandardOutput.Trim()); Assert.AreEqual(5, result3.ExitCode, "Process did not exit successfully"); AssertFalse("process should not return an error", result3.Failed); - ProcessInfo processInfo4 = new ProcessInfo("cmd.exe", "/C @echo Hello World", null, successExitCodes); + ProcessInfo processInfo4 = new ProcessInfo("cmd.exe", "/C @echo Hello World", null, ProcessPriorityClass.AboveNormal, successExitCodes); ProcessResult result4 = executor.Execute(processInfo4); Assert.AreEqual("Hello World", result4.StandardOutput.Trim()); diff --git a/UnitTests/Core/Util/ProcessInfoTest.cs b/UnitTests/Core/Util/ProcessInfoTest.cs index d1fafe3..d27f346 100644 --- a/UnitTests/Core/Util/ProcessInfoTest.cs +++ b/UnitTests/Core/Util/ProcessInfoTest.cs @@ -52,7 +52,7 @@ namespace ThoughtWorks.CruiseControl.UnitTests.Core.Util { int[] successExitCodes = { 1, 3, 5 }; - ProcessInfo info = new ProcessInfo(@"""c:\nant\nant.exe""", null, string.Format(@"""{0}""", Path.GetTempPath()), successExitCodes); + ProcessInfo info = new ProcessInfo(@"""c:\nant\nant.exe""", null, string.Format(@"""{0}""", Path.GetTempPath()), ProcessPriorityClass.Normal, successExitCodes); Assert.IsFalse(info.ProcessSuccessful(0)); Assert.IsTrue(info.ProcessSuccessful(1)); diff --git a/core/sourcecontrol/RobocopySourceControl.cs b/core/sourcecontrol/RobocopySourceControl.cs index de81703..851294c 100644 --- a/core/sourcecontrol/RobocopySourceControl.cs +++ b/core/sourcecontrol/RobocopySourceControl.cs @@ -1,144 +1,145 @@ -using Exortech.NetReflector; -using ThoughtWorks.CruiseControl.Core.Util; - -namespace ThoughtWorks.CruiseControl.Core.Sourcecontrol -{ - /// - /// - /// Uses RoboCopy as Source Control. - /// - /// - /// RoboCopy Source Control Block - /// 1.4.4 - /// - /// The type of source control block. - /// robocopy - /// - /// - /// - /// <sourcecontrol type="repositoryRoot"> - /// <repositoryRoot>C:\Somewhere</repositoryRoot> - /// </sourcecontrol> - /// - /// - [ReflectorType("robocopy")] - public class RobocopySourceControl : ProcessSourceControl - { - private static int[] GenerateExitCodes() - { - int[] exitCodes = new int[4]; - - exitCodes[0] = 0; // All OK, nothing to do - exitCodes[1] = 1; // Some files copied - exitCodes[2] = 2; // Some extra files in destination tree - exitCodes[3] = 3; // Copied and some extra files in destination tree - - // Note that we COULD want to have 4-7 as valid success codes, but i've not been - // able to cause them to occur yet and so havent included them here. - - return exitCodes; - } - - private static readonly int[] successExitCodes = GenerateExitCodes(); - - public RobocopySourceControl() : this(new RobocopyHistoryParser(), new ProcessExecutor()) - {} - - public RobocopySourceControl(IHistoryParser parser, ProcessExecutor executor) : base(parser, executor) - {} - - /// - /// The executable location. - /// - /// 1.4.4 - /// C:\\Windows\\System32\\robocopy.exe - [ReflectorProperty("executable", Required = false)] - public string Executable = "C:\\Windows\\System32\\robocopy.exe"; - - /// - /// The repository root. - /// - /// 1.4.4 - /// n/a - [ReflectorProperty("repositoryRoot")] - public string RepositoryRoot; - - /// - /// Whether to automatically get the source. - /// - /// 1.4.4 - /// false - [ReflectorProperty("autoGetSource", Required = false)] - public bool AutoGetSource = false; - - /// - /// The working directory to use. - /// - /// 1.4.4 - /// Project Working Directory - [ReflectorProperty("workingDirectory", Required = false)] - public string WorkingDirectory = string.Empty; - - /// - /// Any additional arguments. - /// - /// 1.4.4 - /// None - [ReflectorProperty("additionalArguments", Required = false)] - public string AdditionalArguments = string.Empty; - - public override Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to) - { - string destinationDirectory = from.BaseFromWorkingDirectory(WorkingDirectory); - - ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); - - AddStandardArguments(builder, destinationDirectory); - - builder.AddArgument("/L"); - - Modification[] modifications = GetModifications(new ProcessInfo(Executable, builder.ToString(), null, successExitCodes), from.StartTime, to.StartTime); - - return modifications; - } - - public override void LabelSourceControl(IIntegrationResult result) - {} - - public override void GetSource(IIntegrationResult result) - { - if (AutoGetSource) - { - string destinationDirectory = result.BaseFromWorkingDirectory(WorkingDirectory); - - ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); - - AddStandardArguments(builder, destinationDirectory); - - Execute(new ProcessInfo(Executable, builder.ToString(), null, successExitCodes)); - } - } - - // /MIR - MIRror a directory tree (equivalent to /E plus /PURGE). - // /NP - No Progress - don't display % copied. - // /X - Report all eXtra files, not just those selected. - // /TS - Include source file Time Stamps in the output. - // /FP - Include Full Pathname of files in the output. - // /NDL - No Directory List - don't log directory names. - // /NS - No Size - don't log file sizes. - // /NJH - No Job Header. - // /NJS - No Job Summary. - - private readonly static string standardArguments = " /MIR /NP /X /TS /FP /NDL /NS /NJH /NJS "; - - private void AddStandardArguments( - ProcessArgumentBuilder builder, - string destinationDirectory) - { - builder.AddArgument(RepositoryRoot); - builder.AddArgument(destinationDirectory); - builder.Append(standardArguments); - builder.Append(AdditionalArguments); - } - } +using System.Diagnostics; +using Exortech.NetReflector; +using ThoughtWorks.CruiseControl.Core.Util; + +namespace ThoughtWorks.CruiseControl.Core.Sourcecontrol +{ + /// + /// + /// Uses RoboCopy as Source Control. + /// + /// + /// RoboCopy Source Control Block + /// 1.4.4 + /// + /// The type of source control block. + /// robocopy + /// + /// + /// + /// <sourcecontrol type="repositoryRoot"> + /// <repositoryRoot>C:\Somewhere</repositoryRoot> + /// </sourcecontrol> + /// + /// + [ReflectorType("robocopy")] + public class RobocopySourceControl : ProcessSourceControl + { + private static int[] GenerateExitCodes() + { + int[] exitCodes = new int[4]; + + exitCodes[0] = 0; // All OK, nothing to do + exitCodes[1] = 1; // Some files copied + exitCodes[2] = 2; // Some extra files in destination tree + exitCodes[3] = 3; // Copied and some extra files in destination tree + + // Note that we COULD want to have 4-7 as valid success codes, but i've not been + // able to cause them to occur yet and so havent included them here. + + return exitCodes; + } + + private static readonly int[] successExitCodes = GenerateExitCodes(); + + public RobocopySourceControl() : this(new RobocopyHistoryParser(), new ProcessExecutor()) + {} + + public RobocopySourceControl(IHistoryParser parser, ProcessExecutor executor) : base(parser, executor) + {} + + /// + /// The executable location. + /// + /// 1.4.4 + /// C:\\Windows\\System32\\robocopy.exe + [ReflectorProperty("executable", Required = false)] + public string Executable = "C:\\Windows\\System32\\robocopy.exe"; + + /// + /// The repository root. + /// + /// 1.4.4 + /// n/a + [ReflectorProperty("repositoryRoot")] + public string RepositoryRoot; + + /// + /// Whether to automatically get the source. + /// + /// 1.4.4 + /// false + [ReflectorProperty("autoGetSource", Required = false)] + public bool AutoGetSource = false; + + /// + /// The working directory to use. + /// + /// 1.4.4 + /// Project Working Directory + [ReflectorProperty("workingDirectory", Required = false)] + public string WorkingDirectory = string.Empty; + + /// + /// Any additional arguments. + /// + /// 1.4.4 + /// None + [ReflectorProperty("additionalArguments", Required = false)] + public string AdditionalArguments = string.Empty; + + public override Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to) + { + string destinationDirectory = from.BaseFromWorkingDirectory(WorkingDirectory); + + ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); + + AddStandardArguments(builder, destinationDirectory); + + builder.AddArgument("/L"); + + Modification[] modifications = GetModifications(new ProcessInfo(Executable, builder.ToString(), null, ProcessPriorityClass.Normal, successExitCodes), from.StartTime, to.StartTime); + + return modifications; + } + + public override void LabelSourceControl(IIntegrationResult result) + {} + + public override void GetSource(IIntegrationResult result) + { + if (AutoGetSource) + { + string destinationDirectory = result.BaseFromWorkingDirectory(WorkingDirectory); + + ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); + + AddStandardArguments(builder, destinationDirectory); + + Execute(new ProcessInfo(Executable, builder.ToString(), null, ProcessPriorityClass.Normal, successExitCodes)); + } + } + + // /MIR - MIRror a directory tree (equivalent to /E plus /PURGE). + // /NP - No Progress - don't display % copied. + // /X - Report all eXtra files, not just those selected. + // /TS - Include source file Time Stamps in the output. + // /FP - Include Full Pathname of files in the output. + // /NDL - No Directory List - don't log directory names. + // /NS - No Size - don't log file sizes. + // /NJH - No Job Header. + // /NJS - No Job Summary. + + private readonly static string standardArguments = " /MIR /NP /X /TS /FP /NDL /NS /NJH /NJS "; + + private void AddStandardArguments( + ProcessArgumentBuilder builder, + string destinationDirectory) + { + builder.AddArgument(RepositoryRoot); + builder.AddArgument(destinationDirectory); + builder.Append(standardArguments); + builder.Append(AdditionalArguments); + } + } } \ No newline at end of file diff --git a/core/tasks/BaseExecutableTask.cs b/core/tasks/BaseExecutableTask.cs index 2f80361..5f09927 100644 --- a/core/tasks/BaseExecutableTask.cs +++ b/core/tasks/BaseExecutableTask.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Diagnostics; using System.IO; using ThoughtWorks.CruiseControl.Core.Util; @@ -12,6 +13,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks protected abstract string GetProcessFilename(); protected abstract string GetProcessArguments(IIntegrationResult result); protected abstract string GetProcessBaseDirectory(IIntegrationResult result); + protected abstract ProcessPriorityClass GetProcessPriorityClass(); protected abstract int GetProcessTimeout(); protected virtual int[] GetProcessSuccessCodes() @@ -21,7 +23,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks protected virtual ProcessInfo CreateProcessInfo(IIntegrationResult result) { - ProcessInfo info = new ProcessInfo(GetProcessFilename(), GetProcessArguments(result), GetProcessBaseDirectory(result), GetProcessSuccessCodes()); + ProcessInfo info = new ProcessInfo(GetProcessFilename(), GetProcessArguments(result), GetProcessBaseDirectory(result),GetProcessPriorityClass(), GetProcessSuccessCodes()); info.TimeOut = GetProcessTimeout(); IDictionary properties = result.IntegrationProperties; diff --git a/core/tasks/DevenvTask.cs b/core/tasks/DevenvTask.cs index ff876d0..d3c8def 100644 --- a/core/tasks/DevenvTask.cs +++ b/core/tasks/DevenvTask.cs @@ -2,6 +2,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { using System; using System.Collections; + using System.Diagnostics; using System.IO; using System.Text; using Exortech.NetReflector; @@ -61,6 +62,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public const int DEFAULT_BUILD_TIMEOUT = 600; public const string DEFAULT_BUILDTYPE = "rebuild"; public const string DEFAULT_PROJECT = ""; + public const ProcessPriorityClass DEFAULT_PRIORITY = ProcessPriorityClass.Normal; private readonly IRegistry registry; private readonly ProcessExecutor executor; @@ -231,6 +233,14 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks [ReflectorProperty("project", Required = false)] public string Project = DEFAULT_PROJECT; + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = DEFAULT_PRIORITY; + protected override bool Execute(IIntegrationResult result) { result.BuildProgressInformation.SignalStartRunTask(!string.IsNullOrEmpty(Description) ? Description : string.Format("Executing Devenv :{0}", GetArguments())); @@ -246,7 +256,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks private ProcessResult TryToRun(IIntegrationResult result) { - ProcessInfo processInfo = new ProcessInfo(Executable, GetArguments(), result.WorkingDirectory); + ProcessInfo processInfo = new ProcessInfo(Executable, GetArguments(), result.WorkingDirectory, Priority); processInfo.TimeOut = BuildTimeoutSeconds * 1000; IDictionary properties = result.IntegrationProperties; diff --git a/core/tasks/DupFinderTask.cs b/core/tasks/DupFinderTask.cs index 9a30e24..fce6d66 100644 --- a/core/tasks/DupFinderTask.cs +++ b/core/tasks/DupFinderTask.cs @@ -1,522 +1,546 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) 2009 CruiseControl.NET. All rights reserved. -// -//----------------------------------------------------------------------- -using System; - -namespace ThoughtWorks.CruiseControl.Core.Tasks -{ - using System.Diagnostics.CodeAnalysis; - using System.IO; - using Exortech.NetReflector; - using ThoughtWorks.CruiseControl.Core.Util; - using System.Xml; - using System.Collections.Generic; - - /// - /// - /// Check for duplicates using dupfinder (http://duplicatefinder.codeplex.com/). - /// - /// - /// Duplicate Finder Task - /// 1.5 - /// - /// - /// <dupfinder> - /// <fileMask>*.cs</fileMask> - /// <inputDir>Code</inputDir> - /// </dupfinder> - /// - /// - /// <dupfinder> - /// <dynamicValues /> - /// <fileMask>*.cs</fileMask> - /// <includeCode>False</includeCode> - /// <inputDir>Code</inputDir> - /// <recurse>False</recurse> - /// <shortenNames>False</shortenNames> - /// <threshold>5</threshold> - /// <timeout>600</timeout> - /// <width>2</width> - /// </dupfinder> - /// - /// - /// - /// Extended Functionality - /// - /// This task offers some extended functionality over what the base dupfinder executable offers. This extended - /// functionality is primarily intended to add extra value to the web dashboard display. The extended options are: - /// - /// - /// <shortenNames>: This will remove the <inputDir> value from the file names. This means the - /// filenames only contain the relative path to the file, which makes it easier to see where the file is. - /// - /// - /// <includeCode>: This will include the lines of code that were duplicated into the output. These can - /// then be seen in the web dashboard. This meakes it easy to see the code that has been duplicated. - /// - /// - /// These features work by post-processing the XML output from dupfinder. That is, once dupfinder has finished, the - /// task loads the XML file, finds all the elements that need changing and changes them as required. For the code - /// inclusion, it will also open the relevant code files and extract the lines of code as needed. - /// - /// - [ReflectorType("dupfinder")] - public class DupFinderTask - : BaseExecutableTask - { - #region Private consts - [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Private constant")] - private const string DefaultExecutable = "dupfinder"; - #endregion - - #region Private fields - [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Private field")] - private string executable; - #endregion - - #region Constructors - /// - /// Initializes a new instance of the class. - /// - public DupFinderTask() - : this(new ProcessExecutor()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The executor to use. - public DupFinderTask(ProcessExecutor executor) - { - this.executor = executor; - this.TimeOut = 600; - this.Threshold = 5; - this.Width = 2; - } - #endregion - - #region Public properties - #region Executable - /// - /// The executable to use. - /// - /// 1.5 - /// dupfinder - [ReflectorProperty("executable", Required = false)] - public string Executable { get; set; } - #endregion - - #region InputDir - /// - /// The input directory to scan. If relative, this will be relative to the project working directory. - /// - /// 1.5 - /// n/a - [ReflectorProperty("inputDir", Required = true)] - public string InputDir { get; set; } - #endregion - - #region FileMask - /// - /// The file mask to use. - /// - /// 1.5 - /// n/a - [ReflectorProperty("fileMask", Required = true)] - public string FileMask { get; set; } - #endregion - - #region Focus - /// - /// The name of the file to focus on. - /// - /// 1.5 - /// None - [ReflectorProperty("focus", Required = false)] - public string Focus { get; set; } - #endregion - - #region TimeOut - /// - /// The time-out period in seconds. - /// - /// 1.5 - /// 600 - [ReflectorProperty("timeout", Required = false)] - public int TimeOut { get; set; } - #endregion - - #region Threshold - /// - /// The threshold is the number of consecutive lines that have to be the same before it is considered a - /// duplicate. - /// - /// 1.5 - /// 5 - [ReflectorProperty("threshold", Required = false)] - public int Threshold { get; set; } - #endregion - - #region Width - /// - /// The first line of a duplicate must contain at least this many non-white-space characters. - /// - /// 1.5 - /// 2 - [ReflectorProperty("width", Required = false)] - public int Width { get; set; } - #endregion - - #region Recurse - /// - /// To find files that match the filemask in current directory and subdirectories. - /// - /// 1.5 - /// false - [ReflectorProperty("recurse", Required = false)] - public bool Recurse { get; set; } - #endregion - - #region ShortenFileNames - /// - /// Whether to shorten filenames. - /// - /// 1.5 - /// false - [ReflectorProperty("shortenNames", Required = false)] - public bool ShortenFileNames { get; set; } - #endregion - - #region IncludeCode - /// - /// Whether to include the code that has been duplicated. - /// - /// 1.5 - /// false - [ReflectorProperty("includeCode", Required = false)] - public bool IncludeCode { get; set; } - #endregion - - #region LinesToExclude - /// - /// The lines to exclude. - /// - /// 1.5 - /// None - [ReflectorProperty("excludeLines", Required = false)] - public string[] LinesToExclude { get; set; } - #endregion - - #region FilesToExclude - /// - /// The files to exclude. - /// - /// 1.5 - /// None - [ReflectorProperty("excludeFiles", Required = false)] - public string[] FilesToExclude { get; set; } - #endregion - - #region ioSystem - /// - /// Gets or sets the IO system to use. - /// - /// The IO system. - public IFileSystem ioSystem { get; set; } - #endregion - - #region logger - /// - /// Gets or sets the logger to use. - /// - /// The logger. - public ILogger logger { get; private set; } - #endregion - #endregion - - #region Protected methods - #region Execute() - /// - /// Run the task. - /// - /// The result to use. - /// - /// True if the task was successful, false otherwise. - /// - protected override bool Execute(IIntegrationResult result) - { - result.BuildProgressInformation.SignalStartRunTask(!string.IsNullOrEmpty(Description) ? Description : "Executing DupFinder"); - this.logger = this.logger ?? new DefaultLogger(); - - this.executable = string.IsNullOrEmpty(this.Executable) ? DefaultExecutable : this.Executable; - if (!Path.IsPathRooted(this.executable)) - { - this.executable = result.BaseFromWorkingDirectory(this.executable); - this.logger.Debug("Executable changed to " + this.executable); - } - - // Run the executable - this.logger.Info("Executing DupFinder"); - var processResult = TryToRun(CreateProcessInfo(result), result); - - if (this.ShortenFileNames || this.IncludeCode) - { - // Load the results into an XML document - var document = new XmlDocument(); - document.LoadXml(processResult.StandardOutput); - - if (this.IncludeCode) - { - this.logger.Info("Including duplicate code lines"); - this.ioSystem = this.ioSystem ?? new SystemIoFileSystem(); - this.ImportCode(document); - } - - if (this.ShortenFileNames) - { - this.logger.Info("Shortening filenames"); - this.RemoveInputDir(document); - } - - // Generate a new result - processResult = new ProcessResult( - document.OuterXml, - processResult.StandardError, - processResult.ExitCode, - processResult.TimedOut, - processResult.Failed); - } - - // Add the result - result.AddTaskResult(new ProcessTaskResult(processResult, false)); - return !processResult.Failed; - } - #endregion - - #region GetProcessFilename() - /// - /// Retrieve the executable to use. - /// - /// The filename of the process to execute. - protected override string GetProcessFilename() - { - var path = this.QuoteSpaces(this.executable); - return path; - } - #endregion - - #region GetProcessBaseDirectory() - /// - /// Retrieve the base directory. - /// - /// The result to use. - /// The base directory to use. - protected override string GetProcessBaseDirectory(IIntegrationResult result) - { - var path = this.QuoteSpaces(this.InputDir); - return path; - } - #endregion - - #region GetProcessTimeout() - /// - /// Get the time-out period. - /// - /// The time-out period in milliseconds. - protected override int GetProcessTimeout() - { - return this.TimeOut * 1000; - } - #endregion - - #region GetProcessArguments() - /// - /// Retrieve the arguments - /// - /// The result to use. - /// The arguments to pass to the process. - protected override string GetProcessArguments(IIntegrationResult result) - { - var buffer = new ProcessArgumentBuilder(); - buffer.AppendIf(this.Recurse, "-r"); - buffer.AppendArgument("-t" + this.Threshold.ToString()); - buffer.AppendArgument("-w" + this.Width.ToString()); - buffer.AppendArgument("-oConsole"); - - // Add the focus - if (!string.IsNullOrEmpty(this.Focus)) - { - buffer.AppendArgument("-f" + this.QuoteSpaces(this.Focus)); - } - - // Add the lines to exclude - foreach (var line in this.LinesToExclude ?? new string[0]) - { - buffer.AppendArgument("-x" + this.QuoteSpaces(line)); - } - - // Add the lines to exclude - foreach (var line in this.FilesToExclude ?? new string[0]) - { - buffer.AppendArgument("-e" + this.QuoteSpaces(line)); - } - - buffer.AppendArgument(this.FileMask); - return buffer.ToString(); - } - #endregion - - #region RemoveInputDir() - /// - /// Removes the input directory from the filenames. - /// - /// The document containing the data. - protected void RemoveInputDir(XmlDocument document) - { - var duplicateNodes = document.SelectNodes("//Duplicate"); - var length = this.InputDir.Length + 1; - foreach (XmlElement duplicate in duplicateNodes) - { - var filename = duplicate.GetAttribute("FileName"); - if (!string.IsNullOrEmpty(filename) && filename.StartsWith(this.InputDir)) - { - // Store the original, just in case it is needed - duplicate.SetAttribute("OriginalFileName", filename); - duplicate.SetAttribute("FileName", filename.Substring(length)); - } - } - } - #endregion - - #region ImportCode() - /// - /// Imports the duplicated code lines. - /// - /// The document to use. - protected void ImportCode(XmlDocument document) - { - var duplicatesNodes = document.SelectNodes("//Duplicates"); - var fileNames = new Dictionary>(); - - // Get all the file names so each file only needs to be loaded once - this.logger.Debug("Generating file list"); - foreach (XmlElement node in duplicatesNodes) - { - // Check to see if any of the filenames have already been added - var isFound = false; - foreach (XmlElement duplicate in node.ChildNodes) - { - var fileName = duplicate.GetAttribute("FileName"); - if (fileNames.ContainsKey(fileName)) - { - fileNames[fileName].Add(duplicate); - isFound = true; - break; - } - } - - // Add the first filename if the name has not already been added - if (!isFound) - { - var first = node.FirstChild as XmlElement; - var elementList = new List(); - elementList.Add(first); - fileNames.Add(first.GetAttribute("FileName"), elementList); - } - } - - // Load the duplicated code - this.logger.Debug("Importing duplicate code lines"); - foreach (var file in fileNames) - { - // Sort all the lines so the lines can be processed in order - file.Value.Sort(this.CompareFileNodes); - - using (var inputFile = this.ioSystem.OpenInputStream(file.Key)) - { - using (var reader = new StreamReader(inputFile)) - { - var lines = new Dictionary(); - var lineNumber = 1; - string currentLine = null; - foreach (var node in file.Value) - { - // Calculate the lines to read - var parent = node.ParentNode as XmlElement; - var firstLine = Convert.ToInt32(node.GetAttribute("LineNumber")); - var blockLength = Convert.ToInt32(parent.GetAttribute("Length")); - var lastLine = firstLine + blockLength; - - // Move to the first line - while (lineNumber < firstLine) - { - currentLine = reader.ReadLine(); - lineNumber++; - } - - // Read in the lines - while (lineNumber <= lastLine) - { - currentLine = reader.ReadLine(); - lines.Add(lineNumber, currentLine); - lineNumber++; - } - - // Finally, add the lines of code to the XML - var codeNode = document.CreateElement("code"); - parent.AppendChild(codeNode); - for (var loop = firstLine; loop <= lastLine; loop++) - { - var lineNode = document.CreateElement("line"); - codeNode.AppendChild(lineNode); - lineNode.InnerText = lines[loop]; - } - } - } - } - } - } - #endregion - - #region CompareFileNodes() - /// - /// Compares two file nodes. - /// - /// The first node. - /// The second node. - /// - /// Condition Less than 0 firstNode is less than secondNode. 0 firstNode equals secondNode. - /// Greater than 0 firstNode is greater than secondNode. - /// - protected int CompareFileNodes(XmlElement firstNode, XmlElement secondNode) - { - var firstLine = Convert.ToInt32(firstNode.GetAttribute("LineNumber")); - var secondLine = Convert.ToInt32(secondNode.GetAttribute("LineNumber")); - return firstLine - secondLine; - } - #endregion - - #region QuoteSpaces() - /// - /// Adds quotes to a string if it contains spaces. - /// - /// The string to check. - /// The string with quotes if needed. - protected string QuoteSpaces(string value) - { - if (value.Contains(" ") && !value.StartsWith("\"") && !value.EndsWith("\"")) - { - return "\"" + value + "\""; - } - else - { - return value; - } - } - #endregion - #endregion - } -} +//----------------------------------------------------------------------- +// +// Copyright (c) 2009 CruiseControl.NET. All rights reserved. +// +//----------------------------------------------------------------------- +using System; + +namespace ThoughtWorks.CruiseControl.Core.Tasks +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Xml; + using Exortech.NetReflector; + using ThoughtWorks.CruiseControl.Core.Util; + + /// + /// + /// Check for duplicates using dupfinder (http://duplicatefinder.codeplex.com/). + /// + /// + /// Duplicate Finder Task + /// 1.5 + /// + /// + /// <dupfinder> + /// <fileMask>*.cs</fileMask> + /// <inputDir>Code</inputDir> + /// </dupfinder> + /// + /// + /// <dupfinder> + /// <dynamicValues /> + /// <fileMask>*.cs</fileMask> + /// <includeCode>False</includeCode> + /// <inputDir>Code</inputDir> + /// <recurse>False</recurse> + /// <shortenNames>False</shortenNames> + /// <threshold>5</threshold> + /// <timeout>600</timeout> + /// <width>2</width> + /// </dupfinder> + /// + /// + /// + /// Extended Functionality + /// + /// This task offers some extended functionality over what the base dupfinder executable offers. This extended + /// functionality is primarily intended to add extra value to the web dashboard display. The extended options are: + /// + /// + /// <shortenNames>: This will remove the <inputDir> value from the file names. This means the + /// filenames only contain the relative path to the file, which makes it easier to see where the file is. + /// + /// + /// <includeCode>: This will include the lines of code that were duplicated into the output. These can + /// then be seen in the web dashboard. This meakes it easy to see the code that has been duplicated. + /// + /// + /// These features work by post-processing the XML output from dupfinder. That is, once dupfinder has finished, the + /// task loads the XML file, finds all the elements that need changing and changes them as required. For the code + /// inclusion, it will also open the relevant code files and extract the lines of code as needed. + /// + /// + [ReflectorType("dupfinder")] + public class DupFinderTask + : BaseExecutableTask + { + #region Private consts + [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Private constant")] + private const string DefaultExecutable = "dupfinder"; + + /// Default priority class + private const ProcessPriorityClass DefaultPriority = ProcessPriorityClass.Normal; + #endregion + + #region Private fields + [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Private field")] + private string executable; + #endregion + + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public DupFinderTask() + : this(new ProcessExecutor()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The executor to use. + public DupFinderTask(ProcessExecutor executor) + { + this.executor = executor; + this.TimeOut = 600; + this.Threshold = 5; + this.Width = 2; + } + #endregion + + #region Public properties + #region Executable + /// + /// The executable to use. + /// + /// 1.5 + /// dupfinder + [ReflectorProperty("executable", Required = false)] + public string Executable { get; set; } + #endregion + + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; + #endregion + + #region InputDir + /// + /// The input directory to scan. If relative, this will be relative to the project working directory. + /// + /// 1.5 + /// n/a + [ReflectorProperty("inputDir", Required = true)] + public string InputDir { get; set; } + #endregion + + #region FileMask + /// + /// The file mask to use. + /// + /// 1.5 + /// n/a + [ReflectorProperty("fileMask", Required = true)] + public string FileMask { get; set; } + #endregion + + #region Focus + /// + /// The name of the file to focus on. + /// + /// 1.5 + /// None + [ReflectorProperty("focus", Required = false)] + public string Focus { get; set; } + #endregion + + #region TimeOut + /// + /// The time-out period in seconds. + /// + /// 1.5 + /// 600 + [ReflectorProperty("timeout", Required = false)] + public int TimeOut { get; set; } + #endregion + + #region Threshold + /// + /// The threshold is the number of consecutive lines that have to be the same before it is considered a + /// duplicate. + /// + /// 1.5 + /// 5 + [ReflectorProperty("threshold", Required = false)] + public int Threshold { get; set; } + #endregion + + #region Width + /// + /// The first line of a duplicate must contain at least this many non-white-space characters. + /// + /// 1.5 + /// 2 + [ReflectorProperty("width", Required = false)] + public int Width { get; set; } + #endregion + + #region Recurse + /// + /// To find files that match the filemask in current directory and subdirectories. + /// + /// 1.5 + /// false + [ReflectorProperty("recurse", Required = false)] + public bool Recurse { get; set; } + #endregion + + #region ShortenFileNames + /// + /// Whether to shorten filenames. + /// + /// 1.5 + /// false + [ReflectorProperty("shortenNames", Required = false)] + public bool ShortenFileNames { get; set; } + #endregion + + #region IncludeCode + /// + /// Whether to include the code that has been duplicated. + /// + /// 1.5 + /// false + [ReflectorProperty("includeCode", Required = false)] + public bool IncludeCode { get; set; } + #endregion + + #region LinesToExclude + /// + /// The lines to exclude. + /// + /// 1.5 + /// None + [ReflectorProperty("excludeLines", Required = false)] + public string[] LinesToExclude { get; set; } + #endregion + + #region FilesToExclude + /// + /// The files to exclude. + /// + /// 1.5 + /// None + [ReflectorProperty("excludeFiles", Required = false)] + public string[] FilesToExclude { get; set; } + #endregion + + #region ioSystem + /// + /// Gets or sets the IO system to use. + /// + /// The IO system. + public IFileSystem ioSystem { get; set; } + #endregion + + #region logger + /// + /// Gets or sets the logger to use. + /// + /// The logger. + public ILogger logger { get; private set; } + #endregion + #endregion + + #region Protected methods + #region Execute() + /// + /// Run the task. + /// + /// The result to use. + /// + /// True if the task was successful, false otherwise. + /// + protected override bool Execute(IIntegrationResult result) + { + result.BuildProgressInformation.SignalStartRunTask(!string.IsNullOrEmpty(Description) ? Description : "Executing DupFinder"); + this.logger = this.logger ?? new DefaultLogger(); + + this.executable = string.IsNullOrEmpty(this.Executable) ? DefaultExecutable : this.Executable; + if (!Path.IsPathRooted(this.executable)) + { + this.executable = result.BaseFromWorkingDirectory(this.executable); + this.logger.Debug("Executable changed to " + this.executable); + } + + // Run the executable + this.logger.Info("Executing DupFinder"); + var processResult = TryToRun(CreateProcessInfo(result), result); + + if (this.ShortenFileNames || this.IncludeCode) + { + // Load the results into an XML document + var document = new XmlDocument(); + document.LoadXml(processResult.StandardOutput); + + if (this.IncludeCode) + { + this.logger.Info("Including duplicate code lines"); + this.ioSystem = this.ioSystem ?? new SystemIoFileSystem(); + this.ImportCode(document); + } + + if (this.ShortenFileNames) + { + this.logger.Info("Shortening filenames"); + this.RemoveInputDir(document); + } + + // Generate a new result + processResult = new ProcessResult( + document.OuterXml, + processResult.StandardError, + processResult.ExitCode, + processResult.TimedOut, + processResult.Failed); + } + + // Add the result + result.AddTaskResult(new ProcessTaskResult(processResult, false)); + return !processResult.Failed; + } + #endregion + + #region GetProcessFilename() + /// + /// Retrieve the executable to use. + /// + /// The filename of the process to execute. + protected override string GetProcessFilename() + { + var path = this.QuoteSpaces(this.executable); + return path; + } + #endregion + + #region GetProcessBaseDirectory() + /// + /// Retrieve the base directory. + /// + /// The result to use. + /// The base directory to use. + protected override string GetProcessBaseDirectory(IIntegrationResult result) + { + var path = this.QuoteSpaces(this.InputDir); + return path; + } + #endregion + + #region GetProcessTimeout() + /// + /// Get the time-out period. + /// + /// The time-out period in milliseconds. + protected override int GetProcessTimeout() + { + return this.TimeOut * 1000; + } + #endregion + + #region GetProcessArguments() + /// + /// Retrieve the arguments + /// + /// The result to use. + /// The arguments to pass to the process. + protected override string GetProcessArguments(IIntegrationResult result) + { + var buffer = new ProcessArgumentBuilder(); + buffer.AppendIf(this.Recurse, "-r"); + buffer.AppendArgument("-t" + this.Threshold.ToString()); + buffer.AppendArgument("-w" + this.Width.ToString()); + buffer.AppendArgument("-oConsole"); + + // Add the focus + if (!string.IsNullOrEmpty(this.Focus)) + { + buffer.AppendArgument("-f" + this.QuoteSpaces(this.Focus)); + } + + // Add the lines to exclude + foreach (var line in this.LinesToExclude ?? new string[0]) + { + buffer.AppendArgument("-x" + this.QuoteSpaces(line)); + } + + // Add the lines to exclude + foreach (var line in this.FilesToExclude ?? new string[0]) + { + buffer.AppendArgument("-e" + this.QuoteSpaces(line)); + } + + buffer.AppendArgument(this.FileMask); + return buffer.ToString(); + } + #endregion + + #region GetProcessPriorityClass() + /// + /// Gets the requested priority class value for this Task. + /// + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + #endregion + + #region RemoveInputDir() + /// + /// Removes the input directory from the filenames. + /// + /// The document containing the data. + protected void RemoveInputDir(XmlDocument document) + { + var duplicateNodes = document.SelectNodes("//Duplicate"); + var length = this.InputDir.Length + 1; + foreach (XmlElement duplicate in duplicateNodes) + { + var filename = duplicate.GetAttribute("FileName"); + if (!string.IsNullOrEmpty(filename) && filename.StartsWith(this.InputDir)) + { + // Store the original, just in case it is needed + duplicate.SetAttribute("OriginalFileName", filename); + duplicate.SetAttribute("FileName", filename.Substring(length)); + } + } + } + #endregion + + #region ImportCode() + /// + /// Imports the duplicated code lines. + /// + /// The document to use. + protected void ImportCode(XmlDocument document) + { + var duplicatesNodes = document.SelectNodes("//Duplicates"); + var fileNames = new Dictionary>(); + + // Get all the file names so each file only needs to be loaded once + this.logger.Debug("Generating file list"); + foreach (XmlElement node in duplicatesNodes) + { + // Check to see if any of the filenames have already been added + var isFound = false; + foreach (XmlElement duplicate in node.ChildNodes) + { + var fileName = duplicate.GetAttribute("FileName"); + if (fileNames.ContainsKey(fileName)) + { + fileNames[fileName].Add(duplicate); + isFound = true; + break; + } + } + + // Add the first filename if the name has not already been added + if (!isFound) + { + var first = node.FirstChild as XmlElement; + var elementList = new List(); + elementList.Add(first); + fileNames.Add(first.GetAttribute("FileName"), elementList); + } + } + + // Load the duplicated code + this.logger.Debug("Importing duplicate code lines"); + foreach (var file in fileNames) + { + // Sort all the lines so the lines can be processed in order + file.Value.Sort(this.CompareFileNodes); + + using (var inputFile = this.ioSystem.OpenInputStream(file.Key)) + { + using (var reader = new StreamReader(inputFile)) + { + var lines = new Dictionary(); + var lineNumber = 1; + string currentLine = null; + foreach (var node in file.Value) + { + // Calculate the lines to read + var parent = node.ParentNode as XmlElement; + var firstLine = Convert.ToInt32(node.GetAttribute("LineNumber")); + var blockLength = Convert.ToInt32(parent.GetAttribute("Length")); + var lastLine = firstLine + blockLength; + + // Move to the first line + while (lineNumber < firstLine) + { + currentLine = reader.ReadLine(); + lineNumber++; + } + + // Read in the lines + while (lineNumber <= lastLine) + { + currentLine = reader.ReadLine(); + lines.Add(lineNumber, currentLine); + lineNumber++; + } + + // Finally, add the lines of code to the XML + var codeNode = document.CreateElement("code"); + parent.AppendChild(codeNode); + for (var loop = firstLine; loop <= lastLine; loop++) + { + var lineNode = document.CreateElement("line"); + codeNode.AppendChild(lineNode); + lineNode.InnerText = lines[loop]; + } + } + } + } + } + } + #endregion + + #region CompareFileNodes() + /// + /// Compares two file nodes. + /// + /// The first node. + /// The second node. + /// + /// Condition Less than 0 firstNode is less than secondNode. 0 firstNode equals secondNode. + /// Greater than 0 firstNode is greater than secondNode. + /// + protected int CompareFileNodes(XmlElement firstNode, XmlElement secondNode) + { + var firstLine = Convert.ToInt32(firstNode.GetAttribute("LineNumber")); + var secondLine = Convert.ToInt32(secondNode.GetAttribute("LineNumber")); + return firstLine - secondLine; + } + #endregion + + #region QuoteSpaces() + /// + /// Adds quotes to a string if it contains spaces. + /// + /// The string to check. + /// The string with quotes if needed. + protected string QuoteSpaces(string value) + { + if (value.Contains(" ") && !value.StartsWith("\"") && !value.EndsWith("\"")) + { + return "\"" + value + "\""; + } + else + { + return value; + } + } + #endregion + #endregion + } +} diff --git a/core/tasks/ExecutableTask.cs b/core/tasks/ExecutableTask.cs index 35a3be8..1dc9011 100644 --- a/core/tasks/ExecutableTask.cs +++ b/core/tasks/ExecutableTask.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; -using System.Diagnostics; namespace ThoughtWorks.CruiseControl.Core.Tasks { @@ -82,6 +82,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks : BaseExecutableTask { public const int DEFAULT_BUILD_TIMEOUT = 600; + public const ProcessPriorityClass DEFAULT_PRIORITY = ProcessPriorityClass.Normal; public ExecutableTask() : this(new ProcessExecutor()) {} @@ -102,6 +103,14 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string Executable = string.Empty; /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = DEFAULT_PRIORITY; + + /// /// The directory to run the process in. If relative, is a subdirectory of the Project Working /// Directory. /// @@ -239,6 +248,11 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return BuildTimeoutSeconds*1000; } + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + public override string ToString() { return string.Format(@" BaseDirectory: {0}, Executable: {1}", ConfiguredBaseDirectory, Executable); diff --git a/core/tasks/GendarmeTask.cs b/core/tasks/GendarmeTask.cs index 8a6525a..e21d490 100644 --- a/core/tasks/GendarmeTask.cs +++ b/core/tasks/GendarmeTask.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.IO; using System.Text; using Exortech.NetReflector; @@ -56,6 +57,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public const bool defaultVerbose = false; public const bool defaultFailBuildOnFoundDefects = false; public const int defaultVerifyTimeout = 0; + public const ProcessPriorityClass defaultPriority = ProcessPriorityClass.Normal; private readonly IFileDirectoryDeleter fileDirectoryDeleter = new IoService(); @@ -85,6 +87,14 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks [ReflectorProperty("baseDirectory", Required = false)] public string ConfiguredBaseDirectory = string.Empty; + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = defaultPriority; + /// /// Specify the configuration file. /// @@ -238,6 +248,11 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return VerifyTimeoutSeconds * 1000; } + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + protected override bool Execute(IIntegrationResult result) { string gendarmeOutputFile = GetGendarmeOutputFile(result); diff --git a/core/tasks/MsBuildTask.cs b/core/tasks/MsBuildTask.cs index aec661d..201cdbf 100644 --- a/core/tasks/MsBuildTask.cs +++ b/core/tasks/MsBuildTask.cs @@ -1,12 +1,13 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { + using System; using System.Collections; + using System.Diagnostics; using System.IO; + using System.Reflection; using System.Text; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; - using System.Reflection; - using System; /// /// @@ -136,6 +137,17 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks [ReflectorProperty("timeout", Required = false)] public int Timeout = DefaultTimeout; #endregion + + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public string Priority = ProcessPriorityClass.Normal.ToString(); + #endregion + #endregion protected override string GetProcessFilename() @@ -179,6 +191,14 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return Timeout * 1000; } + /// + /// Gets the requested priority class value for this Task. + /// + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return (ProcessPriorityClass)Enum.Parse(typeof(ProcessPriorityClass), this.Priority); + } + protected override bool Execute(IIntegrationResult result) { result.BuildProgressInformation.SignalStartRunTask(!string.IsNullOrEmpty(Description) ? Description : diff --git a/core/tasks/NAntTask.cs b/core/tasks/NAntTask.cs index 8612ce8..1ece0f3 100644 --- a/core/tasks/NAntTask.cs +++ b/core/tasks/NAntTask.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Diagnostics; using System.IO; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; @@ -119,6 +120,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public const string DefaultLogger = "NAnt.Core.XmlLogger"; public const string DefaultListener = "NAnt.Core.DefaultLogger"; public const bool DefaultNoLogo = true; + public const ProcessPriorityClass DefaultPriority = ProcessPriorityClass.Normal; private readonly IFileDirectoryDeleter fileDirectoryDeleter = new IoService(); @@ -154,6 +156,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string Executable = defaultExecutable; #endregion + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = DefaultPriority; + #endregion + #region BuildFile /// /// The name of the build file to run, relative to the baseDirectory. @@ -283,6 +295,11 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { return result.BaseFromWorkingDirectory(ConfiguredBaseDirectory); } + + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } private static void AppendIntegrationResultProperties(ProcessArgumentBuilder buffer, IIntegrationResult result) { diff --git a/core/tasks/NCoverProfileTask.cs b/core/tasks/NCoverProfileTask.cs index 41262d4..8536c34 100644 --- a/core/tasks/NCoverProfileTask.cs +++ b/core/tasks/NCoverProfileTask.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Text; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; -using System.IO; namespace ThoughtWorks.CruiseControl.Core.Tasks { @@ -114,6 +115,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string Executable { get; set; } #endregion + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; + #endregion + #region TimeOut /// /// The time-out period in seconds. If the task does no finish running in this time it will be terminated. @@ -613,6 +624,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return buffer.ToString(); } #endregion + + #region GetProcessPriorityClass() + /// + /// Gets the requested priority class value for this Task. + /// + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + #endregion #endregion #region Private methods diff --git a/core/tasks/NCoverReportTask.cs b/core/tasks/NCoverReportTask.cs index 3a2d9c9..b7f4c06 100644 --- a/core/tasks/NCoverReportTask.cs +++ b/core/tasks/NCoverReportTask.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Text; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; -using System.IO; namespace ThoughtWorks.CruiseControl.Core.Tasks { @@ -122,6 +123,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string WorkingDirectory { get; set; } #endregion + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; + #endregion + #region CoverageFile /// /// The location to read the coverage date from. If relative, this will be relative to baseDir. @@ -499,6 +510,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return buffer.ToString(); } #endregion + + #region GetProcessPriorityClass() + /// + /// Gets the requested priority class value for this Task. + /// + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + #endregion #endregion #region Private methods diff --git a/core/tasks/NDependTask.cs b/core/tasks/NDependTask.cs index d750cb8..afc87d9 100644 --- a/core/tasks/NDependTask.cs +++ b/core/tasks/NDependTask.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Text; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; -using System.IO; namespace ThoughtWorks.CruiseControl.Core.Tasks { @@ -79,6 +80,9 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { #region Private consts private const string defaultExecutable = "NDepend.Console"; + + /// Default priority class + private const ProcessPriorityClass DefaultPriority = ProcessPriorityClass.Normal; #endregion #region Private fields @@ -131,6 +135,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string Executable { get; set; } #endregion + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; + #endregion + #region EmitXml /// /// Whether to emit the XML report data or not. @@ -358,6 +372,16 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return buffer.ToString(); } #endregion + + #region GetProcessPriorityClass() + /// + /// Gets the requested priority class value for this Task. + /// + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + #endregion #endregion #region Private methods diff --git a/core/tasks/NUnitTask.cs b/core/tasks/NUnitTask.cs index 15ada36..e16ff3a 100644 --- a/core/tasks/NUnitTask.cs +++ b/core/tasks/NUnitTask.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.IO; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; @@ -98,7 +99,17 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks /// 1.0 /// 600 [ReflectorProperty("timeout", Required = false)] - public int Timeout = DefaultTimeout; + public int Timeout = DefaultTimeout; + #endregion + + #region Priority + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; #endregion #region ExcludedCategories @@ -150,7 +161,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks Log.Debug(string.Format("Running unit tests: {0} {1}", NUnitPath, args)); - ProcessInfo info = new ProcessInfo(NUnitPath, args, result.WorkingDirectory); + ProcessInfo info = new ProcessInfo(NUnitPath, args, result.WorkingDirectory, Priority); info.TimeOut = Timeout * 1000; return info; } diff --git a/core/tasks/PowerShellTask.cs b/core/tasks/PowerShellTask.cs index 9900a5b..4c22940 100644 --- a/core/tasks/PowerShellTask.cs +++ b/core/tasks/PowerShellTask.cs @@ -1,349 +1,358 @@ -namespace ThoughtWorks.CruiseControl.Core.Tasks -{ - using System; - using System.Collections; - using System.Collections.Specialized; - using System.IO; - using System.Text; - using System.Text.RegularExpressions; - using Exortech.NetReflector; - using ThoughtWorks.CruiseControl.Core.Util; - - /// - /// - /// Runs a PowerShell script. - /// - /// - /// PowerShell Task - /// 1.5 - /// - /// - /// <powershell> - /// <script>dosomething.ps</script> - /// </powershell> - /// - /// - /// <powershell> - /// <script>dosomething.ps</script> - /// <executable>C:\program Files\PowerShell\PowerShell.exe</executable> - /// <scriptsDirectory>C:\Scripts</scriptsDirectory> - /// <buildArgs>-level=1</buildArgs> - /// <environment> - /// <variable name="EnvVar1" value="Some data" /> - /// </environment> - /// <successExitCodes>1,2,3</successExitCodes> - /// <buildTimeoutSeconds>10</buildTimeoutSeconds> - /// <description>Example of how to run a PowerShell script.</description> - /// </powershell> - /// - /// - [ReflectorType("powershell")] - public class PowerShellTask - : TaskBase - { - public const int DefaultBuildTimeOut = 600; - public const string PowerShellExe = "powershell.exe"; - public const string regkeypowershell1 = @"SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"; - public const string regkeypowershell2 = @"SOFTWARE\Microsoft\PowerShell\2\PowerShellEngine"; - public const string regkeyholder = @"ApplicationBase"; - public static string DefaultScriptsDirectory = System.Environment.GetEnvironmentVariable("USERPROFILE") + @"\Documents\WindowsPowerShell\"; - - private string executable; - private ProcessExecutor executor; - - public PowerShellTask() : this(new Registry(), new ProcessExecutor()) { } - - public PowerShellTask(IRegistry registry, ProcessExecutor executor) - { - this.Registry = registry; - this.executor = executor; - } - - /// - /// Expose the registry so the unit tests can change it if necessary. - /// - public IRegistry Registry { get; set; } - - /// - /// The PowerShell script to run. - /// - /// 1.5 - /// None - [ReflectorProperty("script", Required = true)] - public string Script; - - /// - /// The PowerShell executable. - /// - /// 1.5 - /// PowerShell.exe - [ReflectorProperty("executable", Required = false)] - public string Executable - { - get - { - if (executable == null) - { - executable = ReadPowerShellFromRegistry(); - } - return executable; - } - set { executable = value; } - } - - /// - /// The directory that the PowerShell scripts are stored in. - /// - /// 1.5 - /// %Documents%\WindowsPowerShell - [ReflectorProperty("scriptsDirectory", Required = false)] - public string ConfiguredScriptsDirectory = DefaultScriptsDirectory; - - /// - /// Any arguments to pass into the script. - /// - /// 1.5 - /// None - [ReflectorProperty("buildArgs", Required = false)] - public string BuildArgs = string.Empty; - - /// - /// Any environment variables to pass into the script. - /// - /// 1.5 - /// None - [ReflectorArray("environment", Required = false)] - public EnvironmentVariable[] EnvironmentVariables = new EnvironmentVariable[0]; - - private int[] successExitCodes = null; - - /// - /// The exit codes that mark the script as being successful. - /// - /// 1.5 - /// 0 - [ReflectorProperty("successExitCodes", Required = false)] - public string SuccessExitCodes - { - get - { - string result = string.Empty; - if (successExitCodes != null) - { - foreach (int code in successExitCodes) - { - if (result !=string.Empty) - result = result + ","; - result = result + code; - } - } - return result; - } - - set - { - if (!string.IsNullOrEmpty(value)) - { - string[] codes = value.Split(','); - - successExitCodes = new int[codes.Length]; - - for (int i = 0; i < codes.Length; ++i) - { - successExitCodes[i] = Int32.Parse(codes[i]); - } - } - else - { - successExitCodes = null; - } - } - } - - /// - /// The maximum number of seconds the build can take. If the build process takes longer than - /// this period, it will be killed. Specify this value as zero to disable process timeouts. - /// - /// 1.5 - /// 600 - [ReflectorProperty("buildTimeoutSeconds", Required = false)] - public int BuildTimeoutSeconds = DefaultBuildTimeOut; - - /// - /// Run the specified PowerShell and add its output to the build results. - /// - /// the IIntegrationResult object for the build - protected override bool Execute(IIntegrationResult result) - { - result.BuildProgressInformation.SignalStartRunTask(string.Format("Executing {0}", Executable)); - - ProcessInfo processInfo = NewProcessInfoFrom(result); - - ProcessResult processResult = AttemptToExecute(processInfo); - - if (!StringUtil.IsWhitespace(processResult.StandardOutput) || !StringUtil.IsWhitespace(processResult.StandardError)) - { - // The PowerShell produced some output. We need to transform it into an XML build report - // fragment so the rest of CC.Net can process it. - ProcessResult newResult = new ProcessResult( - MakeBuildResult(processResult.StandardOutput,string.Empty), - MakeBuildResult(processResult.StandardError, "Error"), - processResult.ExitCode, - processResult.TimedOut, - processResult.Failed); - - processResult = newResult; - } - result.AddTaskResult(new ProcessTaskResult(processResult)); - - if (processResult.TimedOut) - { - throw new BuilderException(this, "Command Line Build timed out (after " + BuildTimeoutSeconds + " seconds)"); - } - - return !processResult.Failed; - } - - private ProcessInfo NewProcessInfoFrom(IIntegrationResult result) - { - ProcessInfo info = new ProcessInfo( executable, Args(result), BaseDirectory(result), successExitCodes); - info.TimeOut = BuildTimeoutSeconds*1000; - SetConfiguredEnvironmentVariables(info.EnvironmentVariables, this.EnvironmentVariables); - IDictionary properties = result.IntegrationProperties; - foreach (string key in properties.Keys) - { - info.EnvironmentVariables[key] = StringUtil.IntegrationPropertyToString(properties[key]); - } - - return info; - } - - private string BaseDirectory(IIntegrationResult result) - { - return result.BaseFromWorkingDirectory(ConfiguredScriptsDirectory); - } - - protected ProcessResult AttemptToExecute(ProcessInfo info) - { - try - { - return executor.Execute(info); - } - catch (IOException e) - { - throw new BuilderException(this, string.Format("Unable to execute: {0}\n{1}", info, e), e); - } - } - - public override string ToString() - { - return string.Format(@" BaseDirectory: {0}, PowerShell: {1}", ConfiguredScriptsDirectory, PowerShellExe); - } - - /// - /// Convert a stream of text lines separated with newline sequences into an XML build result. - /// - /// the text stream - /// the message level, if any. Values are "Error" and "Warning". - /// the build result string - /// If there are any non-blank lines in the input, they are each wrapped in a - /// <message> element and the entire set is wrapped in a - /// <buildresults> element and returned. Each line of the input is encoded - /// as XML CDATA rules require. If the input is empty or contains only whitspace, an - /// empty string is returned. - /// Note: If we can't manage to understand the input, we just return it unchanged. - /// - private static string MakeBuildResult(string input, string msgLevel) - { - StringBuilder output = new StringBuilder(); - - // Pattern for capturing a line of text, exclusive of the line-ending sequence. - // A "line" is an non-empty unbounded sequence of characters followed by some - // kind of line-ending sequence (CR, LF, or any combination thereof) or - // end-of-string. - Regex linePattern = new Regex(@"([^\r\n]+)"); - - MatchCollection lines = linePattern.Matches(input); - if (lines.Count > 0) - { - output.Append(System.Environment.NewLine); - output.Append(""); - output.Append(System.Environment.NewLine); - foreach (Match line in lines) - { - output.Append(" "); - output.Append(XmlUtil.EncodePCDATA(line.ToString())); - output.Append(""); - output.Append(System.Environment.NewLine); - } - output.Append(""); - output.Append(System.Environment.NewLine); - } - else - output.Append(input); // All of that stuff failed, just return our input - return output.ToString(); - } - - /// - /// Pass the project's environment variables to the process. - /// - /// The collection of environment variables to be updated. - /// An array of environment variables to set. - /// - /// Any variable without a value will be set to an empty string. - /// - private static void SetConfiguredEnvironmentVariables(StringDictionary variablePool, EnvironmentVariable[] varsToSet) - { - foreach (EnvironmentVariable item in varsToSet) - variablePool[item.name] = item.value; - } - - /// - /// Get the name of the PowerShell executable for the highest version installed on this machine. - /// - /// The fully-qualified pathname of the executable. - private string ReadPowerShellFromRegistry() - { - string registryValue = null; - - registryValue = Registry.GetLocalMachineSubKeyValue(regkeypowershell2, regkeyholder); - - if (registryValue == null) - { - registryValue = Registry.GetLocalMachineSubKeyValue(regkeypowershell1, regkeyholder); - } - - if (registryValue == null) - { - Log.Debug("Unable to find PowerShell and it was not defined in Executable Parameter"); - throw new BuilderException(this, "Unable to find PowerShell and it was not defined in Executable Parameter"); - } - - return Path.Combine(registryValue, PowerShellExe); - } - - private string Args(IIntegrationResult result) - { - ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); - - if (!string.IsNullOrEmpty(Script)) - { - if (ConfiguredScriptsDirectory.EndsWith("\\")) - { - builder.AppendArgument(ConfiguredScriptsDirectory + Script); - } - else - { - builder.AppendArgument(ConfiguredScriptsDirectory + "\\" + Script); - } - } - - if (!string.IsNullOrEmpty(BuildArgs)) builder.AppendArgument(BuildArgs); - return builder.ToString(); - } - - } -} +namespace ThoughtWorks.CruiseControl.Core.Tasks +{ + using System; + using System.Collections; + using System.Collections.Specialized; + using System.Diagnostics; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + using Exortech.NetReflector; + using ThoughtWorks.CruiseControl.Core.Util; + + /// + /// + /// Runs a PowerShell script. + /// + /// + /// PowerShell Task + /// 1.5 + /// + /// + /// <powershell> + /// <script>dosomething.ps</script> + /// </powershell> + /// + /// + /// <powershell> + /// <script>dosomething.ps</script> + /// <executable>C:\program Files\PowerShell\PowerShell.exe</executable> + /// <scriptsDirectory>C:\Scripts</scriptsDirectory> + /// <buildArgs>-level=1</buildArgs> + /// <environment> + /// <variable name="EnvVar1" value="Some data" /> + /// </environment> + /// <successExitCodes>1,2,3</successExitCodes> + /// <buildTimeoutSeconds>10</buildTimeoutSeconds> + /// <description>Example of how to run a PowerShell script.</description> + /// </powershell> + /// + /// + [ReflectorType("powershell")] + public class PowerShellTask + : TaskBase + { + public const int DefaultBuildTimeOut = 600; + public const string PowerShellExe = "powershell.exe"; + public const string regkeypowershell1 = @"SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"; + public const string regkeypowershell2 = @"SOFTWARE\Microsoft\PowerShell\2\PowerShellEngine"; + public const string regkeyholder = @"ApplicationBase"; + public static string DefaultScriptsDirectory = System.Environment.GetEnvironmentVariable("USERPROFILE") + @"\Documents\WindowsPowerShell\"; + + private string executable; + private ProcessExecutor executor; + + public PowerShellTask() : this(new Registry(), new ProcessExecutor()) { } + + public PowerShellTask(IRegistry registry, ProcessExecutor executor) + { + this.Registry = registry; + this.executor = executor; + } + + /// + /// Expose the registry so the unit tests can change it if necessary. + /// + public IRegistry Registry { get; set; } + + /// + /// The PowerShell script to run. + /// + /// 1.5 + /// None + [ReflectorProperty("script", Required = true)] + public string Script; + + /// + /// The PowerShell executable. + /// + /// 1.5 + /// PowerShell.exe + [ReflectorProperty("executable", Required = false)] + public string Executable + { + get + { + if (executable == null) + { + executable = ReadPowerShellFromRegistry(); + } + return executable; + } + set { executable = value; } + } + + /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = ProcessPriorityClass.Normal; + + /// + /// The directory that the PowerShell scripts are stored in. + /// + /// 1.5 + /// %Documents%\WindowsPowerShell + [ReflectorProperty("scriptsDirectory", Required = false)] + public string ConfiguredScriptsDirectory = DefaultScriptsDirectory; + + /// + /// Any arguments to pass into the script. + /// + /// 1.5 + /// None + [ReflectorProperty("buildArgs", Required = false)] + public string BuildArgs = string.Empty; + + /// + /// Any environment variables to pass into the script. + /// + /// 1.5 + /// None + [ReflectorArray("environment", Required = false)] + public EnvironmentVariable[] EnvironmentVariables = new EnvironmentVariable[0]; + + private int[] successExitCodes = null; + + /// + /// The exit codes that mark the script as being successful. + /// + /// 1.5 + /// 0 + [ReflectorProperty("successExitCodes", Required = false)] + public string SuccessExitCodes + { + get + { + string result = string.Empty; + if (successExitCodes != null) + { + foreach (int code in successExitCodes) + { + if (result !=string.Empty) + result = result + ","; + result = result + code; + } + } + return result; + } + + set + { + if (!string.IsNullOrEmpty(value)) + { + string[] codes = value.Split(','); + + successExitCodes = new int[codes.Length]; + + for (int i = 0; i < codes.Length; ++i) + { + successExitCodes[i] = Int32.Parse(codes[i]); + } + } + else + { + successExitCodes = null; + } + } + } + + /// + /// The maximum number of seconds the build can take. If the build process takes longer than + /// this period, it will be killed. Specify this value as zero to disable process timeouts. + /// + /// 1.5 + /// 600 + [ReflectorProperty("buildTimeoutSeconds", Required = false)] + public int BuildTimeoutSeconds = DefaultBuildTimeOut; + + /// + /// Run the specified PowerShell and add its output to the build results. + /// + /// the IIntegrationResult object for the build + protected override bool Execute(IIntegrationResult result) + { + result.BuildProgressInformation.SignalStartRunTask(string.Format("Executing {0}", Executable)); + + ProcessInfo processInfo = NewProcessInfoFrom(result); + + ProcessResult processResult = AttemptToExecute(processInfo); + + if (!StringUtil.IsWhitespace(processResult.StandardOutput) || !StringUtil.IsWhitespace(processResult.StandardError)) + { + // The PowerShell produced some output. We need to transform it into an XML build report + // fragment so the rest of CC.Net can process it. + ProcessResult newResult = new ProcessResult( + MakeBuildResult(processResult.StandardOutput,string.Empty), + MakeBuildResult(processResult.StandardError, "Error"), + processResult.ExitCode, + processResult.TimedOut, + processResult.Failed); + + processResult = newResult; + } + result.AddTaskResult(new ProcessTaskResult(processResult)); + + if (processResult.TimedOut) + { + throw new BuilderException(this, "Command Line Build timed out (after " + BuildTimeoutSeconds + " seconds)"); + } + + return !processResult.Failed; + } + + private ProcessInfo NewProcessInfoFrom(IIntegrationResult result) + { + ProcessInfo info = new ProcessInfo( executable, Args(result), BaseDirectory(result), this.Priority, successExitCodes); + info.TimeOut = BuildTimeoutSeconds*1000; + SetConfiguredEnvironmentVariables(info.EnvironmentVariables, this.EnvironmentVariables); + IDictionary properties = result.IntegrationProperties; + foreach (string key in properties.Keys) + { + info.EnvironmentVariables[key] = StringUtil.IntegrationPropertyToString(properties[key]); + } + + return info; + } + + private string BaseDirectory(IIntegrationResult result) + { + return result.BaseFromWorkingDirectory(ConfiguredScriptsDirectory); + } + + protected ProcessResult AttemptToExecute(ProcessInfo info) + { + try + { + return executor.Execute(info); + } + catch (IOException e) + { + throw new BuilderException(this, string.Format("Unable to execute: {0}\n{1}", info, e), e); + } + } + + public override string ToString() + { + return string.Format(@" BaseDirectory: {0}, PowerShell: {1}", ConfiguredScriptsDirectory, PowerShellExe); + } + + /// + /// Convert a stream of text lines separated with newline sequences into an XML build result. + /// + /// the text stream + /// the message level, if any. Values are "Error" and "Warning". + /// the build result string + /// If there are any non-blank lines in the input, they are each wrapped in a + /// <message> element and the entire set is wrapped in a + /// <buildresults> element and returned. Each line of the input is encoded + /// as XML CDATA rules require. If the input is empty or contains only whitspace, an + /// empty string is returned. + /// Note: If we can't manage to understand the input, we just return it unchanged. + /// + private static string MakeBuildResult(string input, string msgLevel) + { + StringBuilder output = new StringBuilder(); + + // Pattern for capturing a line of text, exclusive of the line-ending sequence. + // A "line" is an non-empty unbounded sequence of characters followed by some + // kind of line-ending sequence (CR, LF, or any combination thereof) or + // end-of-string. + Regex linePattern = new Regex(@"([^\r\n]+)"); + + MatchCollection lines = linePattern.Matches(input); + if (lines.Count > 0) + { + output.Append(System.Environment.NewLine); + output.Append(""); + output.Append(System.Environment.NewLine); + foreach (Match line in lines) + { + output.Append(" "); + output.Append(XmlUtil.EncodePCDATA(line.ToString())); + output.Append(""); + output.Append(System.Environment.NewLine); + } + output.Append(""); + output.Append(System.Environment.NewLine); + } + else + output.Append(input); // All of that stuff failed, just return our input + return output.ToString(); + } + + /// + /// Pass the project's environment variables to the process. + /// + /// The collection of environment variables to be updated. + /// An array of environment variables to set. + /// + /// Any variable without a value will be set to an empty string. + /// + private static void SetConfiguredEnvironmentVariables(StringDictionary variablePool, EnvironmentVariable[] varsToSet) + { + foreach (EnvironmentVariable item in varsToSet) + variablePool[item.name] = item.value; + } + + /// + /// Get the name of the PowerShell executable for the highest version installed on this machine. + /// + /// The fully-qualified pathname of the executable. + private string ReadPowerShellFromRegistry() + { + string registryValue = null; + + registryValue = Registry.GetLocalMachineSubKeyValue(regkeypowershell2, regkeyholder); + + if (registryValue == null) + { + registryValue = Registry.GetLocalMachineSubKeyValue(regkeypowershell1, regkeyholder); + } + + if (registryValue == null) + { + Log.Debug("Unable to find PowerShell and it was not defined in Executable Parameter"); + throw new BuilderException(this, "Unable to find PowerShell and it was not defined in Executable Parameter"); + } + + return Path.Combine(registryValue, PowerShellExe); + } + + private string Args(IIntegrationResult result) + { + ProcessArgumentBuilder builder = new ProcessArgumentBuilder(); + + if (!string.IsNullOrEmpty(Script)) + { + if (ConfiguredScriptsDirectory.EndsWith("\\")) + { + builder.AppendArgument(ConfiguredScriptsDirectory + Script); + } + else + { + builder.AppendArgument(ConfiguredScriptsDirectory + "\\" + Script); + } + } + + if (!string.IsNullOrEmpty(BuildArgs)) builder.AppendArgument(BuildArgs); + return builder.ToString(); + } + + } +} diff --git a/core/tasks/RakeTask.cs b/core/tasks/RakeTask.cs index 542b2da..c4324a6 100644 --- a/core/tasks/RakeTask.cs +++ b/core/tasks/RakeTask.cs @@ -1,5 +1,6 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { + using System.Diagnostics; using Exortech.NetReflector; using ThoughtWorks.CruiseControl.Core.Util; @@ -61,6 +62,7 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks { public const int DefaultBuildTimeout = 600; public const string DefaultExecutable = @"rake"; + public const ProcessPriorityClass DefaultPriority = ProcessPriorityClass.Normal; /// /// Any arguments to pass through to Rake (e.g to specify build properties). @@ -105,6 +107,14 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks public string Executable = DefaultExecutable; /// + /// The priority class of the spawned process. + /// + /// 1.5 + /// Normal + [ReflectorProperty("priority", Required = false)] + public ProcessPriorityClass Priority = DefaultPriority; + + /// /// The name of the Rakefile to run, relative to the baseDirectory. /// /// None @@ -215,6 +225,11 @@ namespace ThoughtWorks.CruiseControl.Core.Tasks return Executable; } + protected override ProcessPriorityClass GetProcessPriorityClass() + { + return this.Priority; + } + public string TargetsForPresentation { get diff --git a/core/util/ProcessExecutor.cs b/core/util/ProcessExecutor.cs index 99d90e8..e5bc50d 100644 --- a/core/util/ProcessExecutor.cs +++ b/core/util/ProcessExecutor.cs @@ -142,14 +142,36 @@ namespace ThoughtWorks.CruiseControl.Core.Util process.EnableRaisingEvents = true; supervisingThread = Thread.CurrentThread; - try + string filename = Path.Combine(process.StartInfo.WorkingDirectory, process.StartInfo.FileName); + + try { bool isNewProcess = process.Start(); if (!isNewProcess) Log.Warning("Reusing existing process..."); + + // avoid useless setting of the default + if (processInfo.Priority != System.Diagnostics.Process.GetCurrentProcess().PriorityClass) + { + try + { + Log.Debug(string.Format("Setting PriorityClass on [{0}] to {1}", filename, processInfo.Priority)); + process.PriorityClass = processInfo.Priority; + } + catch (Exception ex) + { + if (!process.HasExited) + { + Log.Info(string.Format("Unable to set PriorityClass on [{0}]: {1}", filename, ex.ToString())); + } + } + } + else + { + Log.Debug(string.Format("Not setting PriorityClass on [{0}] to default {1}", filename, processInfo.Priority)); + } } catch (Win32Exception e) { - string filename = Path.Combine(process.StartInfo.WorkingDirectory, process.StartInfo.FileName); string msg = string.Format("Unable to execute file [{0}]. The file may not exist or may not be executable.", filename); throw new IOException(msg, e); } diff --git a/core/util/ProcessInfo.cs b/core/util/ProcessInfo.cs index ab08024..651389b 100644 --- a/core/util/ProcessInfo.cs +++ b/core/util/ProcessInfo.cs @@ -9,7 +9,10 @@ namespace ThoughtWorks.CruiseControl.Core.Util public class ProcessInfo { public const int DefaultTimeout = 120000; - public const int InfiniteTimeout = 0; + public const int InfiniteTimeout = 0; + + public const ProcessPriorityClass DEFAULT_PRIORITY = ProcessPriorityClass.Normal; + public ProcessPriorityClass Priority; private readonly PrivateArguments arguments; private readonly ProcessStartInfo startInfo = new ProcessStartInfo(); @@ -18,19 +21,23 @@ namespace ThoughtWorks.CruiseControl.Core.Util private readonly int[] successExitCodes; - public ProcessInfo(string filename) : - this(filename, null, null, null){} - - public ProcessInfo(string filename, PrivateArguments arguments) : - this(filename, arguments, null, null){} - - public ProcessInfo(string filename, PrivateArguments arguments, string workingDirectory) : - this(filename, arguments, workingDirectory, null){} - - public ProcessInfo(string filename, PrivateArguments arguments, string workingDirectory, int[] successExitCodes) + public ProcessInfo(string filename) : + this(filename, null){} + + public ProcessInfo(string filename, PrivateArguments arguments) : + this(filename, arguments, null){} + + public ProcessInfo(string filename, PrivateArguments arguments, string workingDirectory) : + this(filename, arguments, workingDirectory, DEFAULT_PRIORITY){} + + public ProcessInfo(string filename, PrivateArguments arguments, string workingDirectory, ProcessPriorityClass priority) : + this(filename, arguments, workingDirectory, priority, null){} + + public ProcessInfo(string filename, PrivateArguments arguments, string workingDirectory, ProcessPriorityClass priority, int[] successExitCodes) { - this.arguments = arguments; - startInfo.FileName = StringUtil.StripQuotes(filename); + this.arguments = arguments; + this.Priority = priority; + startInfo.FileName = StringUtil.StripQuotes(filename); startInfo.Arguments = arguments == null ? null : arguments.ToString(SecureDataMode.Private); startInfo.WorkingDirectory = StringUtil.StripQuotes(workingDirectory); startInfo.UseShellExecute = false; -- 1.6.4.msysgit.0