// 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);
}
}
}
}
}
}