// 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.CodeAnalysis; using System.Globalization; using System.IO; using System.Reflection; using System.Text; using System.Threading; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using WixToolset.Core.CommandLine; /// /// 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); 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 { responseFile = this.GetTemporaryResponseFile(responseFileCommands, out var 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); } } } } } }