From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- src/WixToolset.BuildTasks/WixToolTask.cs | 406 +++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/WixToolset.BuildTasks/WixToolTask.cs (limited to 'src/WixToolset.BuildTasks/WixToolTask.cs') diff --git a/src/WixToolset.BuildTasks/WixToolTask.cs b/src/WixToolset.BuildTasks/WixToolTask.cs new file mode 100644 index 00000000..2e5e8705 --- /dev/null +++ b/src/WixToolset.BuildTasks/WixToolTask.cs @@ -0,0 +1,406 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.BuildTasks +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Text; + using System.Threading; + + using Microsoft.Build.Framework; + using Microsoft.Build.Utilities; + + /// + /// Base class for WiX tool tasks; executes tools in-process + /// so that repeated invocations are much faster. + /// + public abstract class WixToolTask : ToolTask, IDisposable + { + private string additionalOptions; + private bool disposed; + private bool noLogo; + private bool runAsSeparateProcess; + private bool suppressAllWarnings; + private string[] suppressSpecificWarnings; + private string[] treatSpecificWarningsAsErrors; + private bool treatWarningsAsErrors; + private bool verboseOutput; + private Queue messageQueue; + private ManualResetEvent messagesAvailable; + private ManualResetEvent toolExited; + private int exitCode; + + /// + /// Gets or sets additional options that are appended the the tool command-line. + /// + /// + /// This allows the task to support extended options in the tool which are not + /// explicitly implemented as properties on the task. + /// + public string AdditionalOptions + { + get { return this.additionalOptions; } + set { this.additionalOptions = value; } + } + + /// + /// Gets or sets a flag indicating whether the task should be run as separate + /// process instead of in-proc with MSBuild which is the default. + /// + public bool RunAsSeparateProcess + { + get { return this.runAsSeparateProcess; } + set { this.runAsSeparateProcess = value; } + } + +#region Common Options + /// + /// Gets or sets whether all warnings should be suppressed. + /// + public bool SuppressAllWarnings + { + get { return this.suppressAllWarnings; } + set { this.suppressAllWarnings = value; } + } + + /// + /// Gets or sets a list of specific warnings to be suppressed. + /// + public string[] SuppressSpecificWarnings + { + get { return this.suppressSpecificWarnings; } + set { this.suppressSpecificWarnings = value; } + } + + /// + /// Gets or sets whether all warnings should be treated as errors. + /// + public bool TreatWarningsAsErrors + { + get { return this.treatWarningsAsErrors; } + set { this.treatWarningsAsErrors = value; } + } + + /// + /// Gets or sets a list of specific warnings to treat as errors. + /// + public string[] TreatSpecificWarningsAsErrors + { + get { return this.treatSpecificWarningsAsErrors; } + set { this.treatSpecificWarningsAsErrors = value; } + } + + /// + /// Gets or sets whether to display verbose output. + /// + public bool VerboseOutput + { + get { return this.verboseOutput; } + set { this.verboseOutput = value; } + } + + /// + /// Gets or sets whether to display the logo. + /// + public bool NoLogo + { + get { return this.noLogo; } + set { this.noLogo = value; } + } +#endregion + + /// + /// Cleans up the ManualResetEvent members + /// + public void Dispose() + { + if (!this.disposed) + { + this.Dispose(true); + GC.SuppressFinalize(this); + disposed = true; + } + } + + /// + /// Cleans up the ManualResetEvent members + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + messagesAvailable.Close(); + toolExited.Close(); + } + } + + /// + /// Generate the command line arguments to write to the response file from the properties. + /// + /// Command line string. + protected override string GenerateResponseFileCommands() + { + WixCommandLineBuilder commandLineBuilder = new WixCommandLineBuilder(); + this.BuildCommandLine(commandLineBuilder); + return commandLineBuilder.ToString(); + } + + /// + /// Builds a command line from options in this and derivative tasks. + /// + /// + /// Derivative classes should call BuildCommandLine() on the base class to ensure that common command line options are added to the command. + /// + protected virtual void BuildCommandLine(WixCommandLineBuilder commandLineBuilder) + { + commandLineBuilder.AppendIfTrue("-nologo", this.NoLogo); + commandLineBuilder.AppendArrayIfNotNull("-sw", this.SuppressSpecificWarnings); + commandLineBuilder.AppendIfTrue("-sw", this.SuppressAllWarnings); + commandLineBuilder.AppendIfTrue("-v", this.VerboseOutput); + commandLineBuilder.AppendArrayIfNotNull("-wx", this.TreatSpecificWarningsAsErrors); + commandLineBuilder.AppendIfTrue("-wx", this.TreatWarningsAsErrors); + } + + /// + /// Executes a tool in-process by loading the tool assembly and invoking its entrypoint. + /// + /// Path to the tool to be executed; must be a managed executable. + /// Commands to be written to a response file. + /// Commands to be passed directly on the command-line. + /// The tool exit code. + protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) + { + if (this.RunAsSeparateProcess) + { + return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); + } + + this.messageQueue = new Queue(); + this.messagesAvailable = new ManualResetEvent(false); + this.toolExited = new ManualResetEvent(false); + + Util.RunningInMsBuild = true; + + WixToolTaskLogger logger = new WixToolTaskLogger(this.messageQueue, this.messagesAvailable); + TextWriter saveConsoleOut = Console.Out; + TextWriter saveConsoleError = Console.Error; + Console.SetOut(logger); + Console.SetError(logger); + + string responseFile = null; + try + { + string responseFileSwitch; + responseFile = this.GetTemporaryResponseFile(responseFileCommands, out responseFileSwitch); + if (!String.IsNullOrEmpty(responseFileSwitch)) + { + commandLineCommands = commandLineCommands + " " + responseFileSwitch; + } + + string[] arguments = CommandLineResponseFile.ParseArgumentsToArray(commandLineCommands); + + Thread toolThread = new Thread(new ParameterizedThreadStart(this.ExecuteToolThread)); + toolThread.Start(new object[] { pathToTool, arguments }); + + this.HandleToolMessages(); + + if (this.exitCode == 0 && this.Log.HasLoggedErrors) + { + this.exitCode = -1; + } + + return this.exitCode; + } + finally + { + if (responseFile != null) + { + File.Delete(responseFile); + } + + Console.SetOut(saveConsoleOut); + Console.SetError(saveConsoleError); + } + } + + /// + /// Called by a new thread to execute the tool in that thread. + /// + /// Tool path and arguments array. + private void ExecuteToolThread(object parameters) + { + try + { + object[] pathAndArguments = (object[])parameters; + Assembly toolAssembly = Assembly.LoadFrom((string)pathAndArguments[0]); + this.exitCode = (int)toolAssembly.EntryPoint.Invoke(null, new object[] { pathAndArguments[1] }); + } + catch (FileNotFoundException fnfe) + { + Log.LogError("Unable to load tool from path {0}. Consider setting the ToolPath parameter to $(WixToolPath).", fnfe.FileName); + this.exitCode = -1; + } + catch (Exception ex) + { + this.exitCode = -1; + this.LogEventsFromTextOutput(ex.Message, MessageImportance.High); + foreach (string stackTraceLine in ex.StackTrace.Split('\n')) + { + this.LogEventsFromTextOutput(stackTraceLine.TrimEnd(), MessageImportance.High); + } + + throw; + } + finally + { + this.toolExited.Set(); + } + } + + /// + /// Waits for messages from the tool thread and sends them to the MSBuild logger on the original thread. + /// Returns when the tool thread exits. + /// + private void HandleToolMessages() + { + WaitHandle[] waitHandles = new WaitHandle[] { this.messagesAvailable, this.toolExited }; + while (WaitHandle.WaitAny(waitHandles) == 0) + { + lock (this.messageQueue) + { + while (this.messageQueue.Count > 0) + { + this.LogEventsFromTextOutput(messageQueue.Dequeue(), MessageImportance.Normal); + } + + this.messagesAvailable.Reset(); + } + } + } + + /// + /// Creates a temporary response file for tool execution. + /// + /// Path to the response file. + /// + /// The temporary file should be deleted after the tool execution is finished. + /// + private string GetTemporaryResponseFile(string responseFileCommands, out string responseFileSwitch) + { + string responseFile = null; + responseFileSwitch = null; + + if (!String.IsNullOrEmpty(responseFileCommands)) + { + responseFile = Path.GetTempFileName(); + using (StreamWriter writer = new StreamWriter(responseFile, false, this.ResponseFileEncoding)) + { + writer.Write(responseFileCommands); + } + responseFileSwitch = this.GetResponseFileSwitch(responseFile); + } + return responseFile; + } + + /// + /// Cycles thru each task to find correct path of the file in question. + /// Looks at item spec, hintpath and then in user defined Reference Paths + /// + /// Input task array + /// SemiColon delimited directories to search + /// List of task item file paths + [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")] + protected static List AdjustFilePaths(ITaskItem[] tasks, string[] referencePaths) + { + List sourceFilePaths = new List(); + + if (tasks == null) + { + return sourceFilePaths; + } + + foreach (ITaskItem task in tasks) + { + string filePath = task.ItemSpec; + if (!File.Exists(filePath)) + { + filePath = task.GetMetadata("HintPath"); + if (!File.Exists(filePath)) + { + string searchPath = FileSearchHelperMethods.SearchFilePaths(referencePaths, filePath); + if (!String.IsNullOrEmpty(searchPath)) + { + filePath = searchPath; + } + } + } + sourceFilePaths.Add(filePath); + } + + return sourceFilePaths; + } + + /// + /// Used as a replacement for Console.Out to capture output from a tool + /// and redirect it to the MSBuild logging system. + /// + private class WixToolTaskLogger : TextWriter + { + private StringBuilder buffer; + private Queue messageQueue; + private ManualResetEvent messagesAvailable; + + /// + /// Creates a new logger that sends tool output to the tool task's log handler. + /// + public WixToolTaskLogger(Queue messageQueue, ManualResetEvent messagesAvailable) : base(CultureInfo.CurrentCulture) + { + this.messageQueue = messageQueue; + this.messagesAvailable = messagesAvailable; + this.buffer = new StringBuilder(); + } + + /// + /// Gets the encoding of the logger. + /// + public override Encoding Encoding + { + get { return Encoding.Unicode; } + } + + /// + /// Redirects output to a buffer; watches for newlines and sends each line to the + /// MSBuild logging system. + /// + /// Character being written. + /// All other Write() variants eventually call into this one. + public override void Write(char value) + { + lock (this.messageQueue) + { + if (value == '\n') + { + if (this.buffer.Length > 0 && this.buffer[this.buffer.Length - 1] == '\r') + { + this.buffer.Length = this.buffer.Length - 1; + } + + this.messageQueue.Enqueue(this.buffer.ToString()); + this.messagesAvailable.Set(); + + this.buffer.Length = 0; + } + else + { + this.buffer.Append(value); + } + } + } + } + } +} -- cgit v1.2.3-55-g6feb