File: ToolTask.cs
Web Access
Project: ..\..\..\src\Utilities\Microsoft.Build.Utilities.csproj (Microsoft.Build.Utilities.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Resources;
using System.Text;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
 
#nullable disable
 
namespace Microsoft.Build.Utilities
{
    /// <summary>
    /// The return value from InitializeHostObject.  This enumeration defines what action the ToolTask
    /// should take next, after we've tried to initialize the host object.
    /// </summary>
    public enum HostObjectInitializationStatus
    {
        /// <summary>
        /// This means that there exists an appropriate host object for this task, it can support
        /// all of the parameters passed in, and it should be invoked to do the real work of the task.
        /// </summary>
        UseHostObjectToExecute,
 
        /// <summary>
        /// This means that either there is no host object available, or that the host object is
        /// not capable of supporting all of the features required for this build.  Therefore,
        /// ToolTask should fallback to an alternate means of doing the build, such as invoking
        /// the command-line tool.
        /// </summary>
        UseAlternateToolToExecute,
 
        /// <summary>
        /// This means that the host object is already up-to-date, and no further action is necessary.
        /// </summary>
        NoActionReturnSuccess,
 
        /// <summary>
        /// This means that some of the parameters being passed into the task are invalid, and the
        /// task should fail immediately.
        /// </summary>
        NoActionReturnFailure
    }
 
    /// <summary>
    /// Base class used for tasks that spawn an executable. This class implements the ToolPath property which can be used to
    /// override the default path.
    /// </summary>
    // INTERNAL WARNING: DO NOT USE the Log property in this class! Log points to resources in the task assembly itself, and
    // we want to use resources from Utilities. Use LogPrivate (for private Utilities resources) and LogShared (for shared MSBuild resources)
    public abstract class ToolTask : Task, IIncrementalTask, ICancelableTask
    {
        private static readonly bool s_preserveTempFiles = string.Equals(Environment.GetEnvironmentVariable("MSBUILDPRESERVETOOLTEMPFILES"), "1", StringComparison.Ordinal);
 
        #region Constructors
 
        /// <summary>
        /// Protected constructor
        /// </summary>
        protected ToolTask()
        {
            LogPrivate = new TaskLoggingHelper(this)
            {
                TaskResources = AssemblyResources.PrimaryResources,
                HelpKeywordPrefix = "MSBuild."
            };
 
            LogShared = new TaskLoggingHelper(this)
            {
                TaskResources = AssemblyResources.SharedResources,
                HelpKeywordPrefix = "MSBuild."
            };
 
            // 5 second is the default termination timeout.
            TaskProcessTerminationTimeout = 5000;
            ToolCanceled = new ManualResetEvent(false);
        }
 
        /// <summary>
        /// Protected constructor
        /// </summary>
        /// <param name="taskResources">The resource manager for task resources</param>
        protected ToolTask(ResourceManager taskResources)
            : this()
        {
            TaskResources = taskResources;
        }
 
        /// <summary>
        /// Protected constructor
        /// </summary>
        /// <param name="taskResources">The resource manager for task resources</param>
        /// <param name="helpKeywordPrefix">The help keyword prefix for task's messages</param>
        protected ToolTask(ResourceManager taskResources, string helpKeywordPrefix)
            : this(taskResources)
        {
            HelpKeywordPrefix = helpKeywordPrefix;
        }
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// The return code of the spawned process. If the task logged any errors, but the process
        /// had an exit code of 0 (success), this will be set to -1.
        /// </summary>
        [Output]
        public int ExitCode { get; private set; }
 
        /// <summary>
        /// When set to true, this task will yield the node when its task is executing.
        /// </summary>
        public bool YieldDuringToolExecution { get; set; }
 
        /// <summary>
        /// When set to true, the tool task will create a batch file for the command-line and execute that using the command-processor,
        /// rather than executing the command directly.
        /// </summary>
        public bool UseCommandProcessor { get; set; }
 
        /// <summary>
        /// When set to true, it passes /Q to the cmd.exe command line such that the command line does not get echo-ed on stdout
        /// </summary>
        public bool EchoOff { get; set; }
 
        /// <summary>
        /// A timeout to wait for a task to terminate before killing it.  In milliseconds.
        /// </summary>
        protected int TaskProcessTerminationTimeout { get; set; }
 
        /// <summary>
        /// Used to signal when a tool has been cancelled.
        /// </summary>
        protected ManualResetEvent ToolCanceled { get; private set; }
 
        /// <summary>
        /// This is the batch file created when UseCommandProcessor is set to true.
        /// </summary>
        private string _temporaryBatchFile;
 
        /// <summary>
        /// The encoding set to the console code page.
        /// </summary>
        private Encoding _encoding;
 
        /// <summary>
        /// Implemented by the derived class. Returns a string which is the name of the underlying .EXE to run e.g. "resgen.exe"
        /// Only used by the ToolExe getter.
        /// </summary>
        /// <value>Name of tool.</value>
        protected abstract string ToolName { get; }
 
        /// <summary>
        /// Projects may set this to override a task's ToolName.
        /// Tasks may override this to prevent that.
        /// </summary>
        public virtual string ToolExe
        {
            get
            {
                if (!string.IsNullOrEmpty(_toolExe))
                {
                    // If the ToolExe has been overridden then return the value
                    return _toolExe;
                }
                else
                {
                    // We have no override, so simply delegate to ToolName
                    return ToolName;
                }
            }
            set => _toolExe = value;
        }
 
        /// <summary>
        /// Project-visible property allows the user to override the path to the executable.
        /// </summary>
        /// <value>Path to tool.</value>
        public string ToolPath { set; get; }
 
        /// <summary>
        /// Whether or not to use UTF8 encoding for the cmd file and console window.
        /// Values: Always, Never, Detect
        /// If set to Detect, the current code page will be used unless it cannot represent
        /// the Command string. In that case, UTF-8 is used.
        /// </summary>
        public string UseUtf8Encoding { get; set; } = EncodingUtilities.UseUtf8Detect;
 
        /// <summary>
        /// Array of equals-separated pairs of environment
        /// variables that should be passed to the spawned executable,
        /// in addition to (or selectively overriding) the regular environment block.
        /// </summary>
        /// <remarks>
        /// Using this instead of EnvironmentOverride as that takes a Dictionary,
        /// which cannot be set from an MSBuild project.
        /// </remarks>
        public string[] EnvironmentVariables { get; set; }
 
        /// <summary>
        /// Project visible property that allows the user to specify an amount of time after which the task executable
        /// is terminated.
        /// </summary>
        /// <value>Time-out in milliseconds. Default is <see cref="System.Threading.Timeout.Infinite"/> (no time-out).</value>
        public virtual int Timeout { set; get; } = System.Threading.Timeout.Infinite;
 
        /// <summary>
        /// Overridable property specifying the encoding of the response file, UTF8 by default
        /// </summary>
        protected virtual Encoding ResponseFileEncoding => Encoding.UTF8;
 
        /// <summary>
        /// Overridable method to escape content of the response file
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Shipped this way in Dev11 Beta (go-live)")]
        protected virtual string ResponseFileEscape(string responseString) => responseString;
 
        /// <summary>
        /// Overridable property specifying the encoding of the captured task standard output stream
        /// </summary>
        /// <remarks>
        /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding
        /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding
        /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application).
        /// </remarks>
        protected virtual Encoding StandardOutputEncoding
        {
            get
            {
                if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))
                {
                    if (_encoding != null)
                    {
                        // Keep the encoding of standard output & error consistent with the console code page.
                        return _encoding;
                    }
                }
                return EncodingUtilities.CurrentSystemOemEncoding;
            }
        }
 
        /// <summary>
        /// Overridable property specifying the encoding of the captured task standard error stream
        /// </summary>
        /// <remarks>
        /// Console-based output uses the current system OEM code page by default. Note that we should not use Console.OutputEncoding
        /// here since processes we run don't really have much to do with our console window (and also Console.OutputEncoding
        /// doesn't return the OEM code page if the running application that hosts MSBuild is not a console application).
        /// </remarks>
        protected virtual Encoding StandardErrorEncoding
        {
            get
            {
                if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))
                {
                    if (_encoding != null)
                    {
                        // Keep the encoding of standard output & error consistent with the console code page.
                        return _encoding;
                    }
                }
                return EncodingUtilities.CurrentSystemOemEncoding;
            }
        }
 
        /// <summary>
        /// Gets the Path override value.
        /// </summary>
        /// <returns>The new value for the Environment for the task.</returns>
        [Obsolete("Use EnvironmentVariables property")]
        protected virtual Dictionary<string, string> EnvironmentOverride => null;
 
        /// <summary>
        /// Importance with which to log text from the
        /// standard error stream.
        /// </summary>
        protected virtual MessageImportance StandardErrorLoggingImportance => MessageImportance.Normal;
 
        /// <summary>
        /// Whether this ToolTask has logged any errors
        /// </summary>
        protected virtual bool HasLoggedErrors => Log.HasLoggedErrors || LogPrivate.HasLoggedErrors || LogShared.HasLoggedErrors;
 
        /// <summary>
        /// Task Parameter: Importance with which to log text from the
        /// standard out stream.
        /// </summary>
        public string StandardOutputImportance { get; set; } = null;
 
        /// <summary>
        /// Task Parameter: Importance with which to log text from the
        /// standard error stream.
        /// </summary>
        public string StandardErrorImportance { get; set; } = null;
 
        /// <summary>
        /// Should ALL messages received on the standard error stream be logged as errors.
        /// </summary>
        public bool LogStandardErrorAsError { get; set; } = false;
 
        /// <summary>
        /// Importance with which to log text from in the
        /// standard out stream.
        /// </summary>
        protected virtual MessageImportance StandardOutputLoggingImportance => MessageImportance.Low;
 
        /// <summary>
        /// The actual importance at which standard out messages will be logged.
        /// </summary>
        protected MessageImportance StandardOutputImportanceToUse => _standardOutputImportanceToUse;
 
        /// <summary>
        /// The actual importance at which standard error messages will be logged.
        /// </summary>
        protected MessageImportance StandardErrorImportanceToUse => _standardErrorImportanceToUse;
 
        #endregion
 
        #region Private properties
 
        /// <summary>
        /// Gets an instance of a private TaskLoggingHelper class containing task logging methods.
        /// This is necessary because ToolTask lives in a different assembly than the task inheriting from it
        /// and needs its own separate resources.
        /// </summary>
        /// <value>The logging helper object.</value>
        private TaskLoggingHelper LogPrivate { get; }
 
        // the private logging helper
 
        /// <summary>
        /// Gets an instance of a shared resources TaskLoggingHelper class containing task logging methods.
        /// This is necessary because ToolTask lives in a different assembly than the task inheriting from it
        /// and needs its own separate resources.
        /// </summary>
        /// <value>The logging helper object.</value>
        private TaskLoggingHelper LogShared { get; }
 
        // the shared resources logging helper
 
        #endregion
 
        #region Overridable methods
 
        /// <summary>
        /// Overridable function called after <see cref="Process.Start()"/> in <see cref="ExecuteTool"/>
        /// </summary>
        protected virtual void ProcessStarted() { }
 
        /// <summary>
        /// Gets the fully qualified tool name. Should return ToolExe if ToolTask should search for the tool
        /// in the system path. If ToolPath is set, this is ignored.
        /// </summary>
        /// <returns>Path string.</returns>
        protected abstract string GenerateFullPathToTool();
 
        /// <summary>
        /// Gets the working directory to use for the process. Should return null if ToolTask should use the
        /// current directory.
        /// </summary>
        /// <remarks>This is a method rather than a property so that derived classes (like Exec) can choose to
        /// expose a public WorkingDirectory property, and it would be confusing to have two properties.</remarks>
        /// <returns></returns>
        protected virtual string GetWorkingDirectory() => null;
 
        /// <summary>
        /// Implemented in the derived class
        /// </summary>
        /// <returns>true, if successful</returns>
        protected internal virtual bool ValidateParameters()
        {
            if (TaskProcessTerminationTimeout < -1 && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6))
            {
                Log.LogWarningWithCodeFromResources("ToolTask.InvalidTerminationTimeout", TaskProcessTerminationTimeout);
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Returns true if task execution is not necessary. Executed after ValidateParameters
        /// </summary>
        /// <returns></returns>
        protected virtual bool SkipTaskExecution() { canBeIncremental = false; return false; }
 
        /// <summary>
        /// ToolTask is not incremental by default. When a derived class overrides SkipTaskExecution, then Question feature can take into effect.
        /// </summary>
        protected bool canBeIncremental { get; set; } = true;
 
        public bool FailIfNotIncremental { get; set; }
 
        /// <summary>
        /// Returns a string with those switches and other information that can go into a response file.
        /// Called after ValidateParameters and SkipTaskExecution
        /// </summary>
        /// <returns></returns>
        protected virtual string GenerateResponseFileCommands() => string.Empty; // Default is nothing. This is useful for tools that don't need or support response files.
 
        /// <summary>
        /// Returns a string with those switches and other information that can't go into a response file and
        /// must go directly onto the command line.
        /// Called after ValidateParameters and SkipTaskExecution
        /// </summary>
        /// <returns></returns>
        protected virtual string GenerateCommandLineCommands() => string.Empty; // Default is nothing. This is useful for tools where all the parameters can go into a response file.
 
        /// <summary>
        /// Returns the command line switch used by the tool executable to specify the response file.
        /// Will only be called if the task returned a non empty string from GetResponseFileCommands
        /// Called after ValidateParameters, SkipTaskExecution and GetResponseFileCommands
        /// </summary>
        /// <param name="responseFilePath">full path to the temporarily created response file</param>
        /// <returns></returns>
        protected virtual string GetResponseFileSwitch(string responseFilePath) => "@\"" + responseFilePath + "\""; // by default, return @"<responseFilePath>"
 
        /// <summary>
        /// Allows tool to handle the return code.
        /// This method will only be called with non-zero exitCode.
        /// </summary>
        /// <returns>The return value of this method will be used as the task return value</returns>
        protected virtual bool HandleTaskExecutionErrors()
        {
            Debug.Assert(ExitCode != 0, "HandleTaskExecutionErrors should only be called if there were problems executing the task");
 
            if (HasLoggedErrors)
            {
                // Emit a message.
                LogPrivate.LogMessageFromResources(MessageImportance.Low, "General.ToolCommandFailedNoErrorCode", ExitCode);
            }
            else
            {
                // If the tool itself did not log any errors on its own, then we log one now simply saying
                // that the tool exited with a non-zero exit code.  This way, the customer nevers sees
                // "Build failed" without at least one error being logged.
                LogPrivate.LogErrorWithCodeFromResources("ToolTask.ToolCommandFailed", ToolExe, ExitCode);
            }
 
            // by default, always fail the task
            return false;
        }
 
        /// <summary>
        /// We expect the tasks to override this method, if they support host objects. The implementation should call into the
        /// host object to perform the real work of the task. For example, for compiler tasks like Csc and Vbc, this method would
        /// call Compile() on the host object.
        /// </summary>
        /// <returns>The return value indicates success (true) or failure (false) if the host object was actually called to do the work.</returns>
        protected virtual bool CallHostObjectToExecute() => false;
 
        /// <summary>
        /// We expect tasks to override this method if they support host objects.  The implementation should
        /// make sure that the host object is ready to perform the real work of the task.
        /// </summary>
        /// <returns>The return value indicates what steps to take next.  The default is to assume that there
        /// is no host object provided, and therefore we should fallback to calling the command-line tool.</returns>
        protected virtual HostObjectInitializationStatus InitializeHostObject() => HostObjectInitializationStatus.UseAlternateToolToExecute;
 
        /// <summary>
        /// Logs the actual command line about to be executed (or what the task wants the log to show)
        /// </summary>
        /// <param name="message">
        /// Descriptive message about what is happening - usually the command line to be executed.
        /// </param>
        protected virtual void LogToolCommand(string message) => LogPrivate.LogCommandLine(MessageImportance.High, message); // Log a descriptive message about what's happening.
 
        /// <summary>
        /// Logs the tool name and the path from where it is being run.
        /// </summary>
        /// <param name="toolName">
        /// The tool to Log. This is the actual tool being used, ie. if ToolExe has been specified it will be used, otherwise it will be ToolName
        /// </param>
        /// <param name="pathToTool">
        /// The path from where the tool is being run.
        /// </param>
        protected virtual void LogPathToTool(string toolName, string pathToTool)
        {
            // We don't do anything here any more, as it was just duplicative and noise.
            // The method only remains for backwards compatibility - to avoid breaking tasks that override it
        }
 
        #endregion
 
        #region Methods
 
        /// <summary>
        /// Figures out the path to the tool (including the .exe), either by using the ToolPath
        /// parameter, or by asking the derived class to tell us where it should be located.
        /// </summary>
        /// <returns>path to the tool, or null</returns>
        private string ComputePathToTool()
        {
            string pathToTool = null;
 
            if (UseCommandProcessor)
            {
                return ToolExe;
            }
 
            if (!string.IsNullOrEmpty(ToolPath))
            {
                // If the project author passed in a ToolPath, always use that.
                pathToTool = Path.Combine(ToolPath, ToolExe);
            }
 
            if (string.IsNullOrWhiteSpace(pathToTool) || (ToolPath == null && !FileSystems.Default.FileExists(pathToTool)))
            {
                // Otherwise, try to find the tool ourselves.
                pathToTool = GenerateFullPathToTool();
 
                // We have no toolpath, but we have been given an override
                // for the tool exe, fix up the path, assuming that the tool is in the same location
                if (pathToTool != null && !string.IsNullOrEmpty(_toolExe))
                {
                    string directory = Path.GetDirectoryName(pathToTool);
                    pathToTool = Path.Combine(directory, ToolExe);
                }
            }
 
            // only look for the file if we have a path to it. If we have just the file name, we'll
            // look for it in the path
            if (pathToTool != null)
            {
                bool isOnlyFileName = Path.GetFileName(pathToTool).Length == pathToTool.Length;
                if (!isOnlyFileName)
                {
                    bool isExistingFile = FileSystems.Default.FileExists(pathToTool);
                    if (!isExistingFile)
                    {
                        LogPrivate.LogErrorWithCodeFromResources("ToolTask.ToolExecutableNotFound", pathToTool);
                        return null;
                    }
                }
                else
                {
                    // if we just have the file name, search for the file on the system path
                    string actualPathToTool = FindOnPath(pathToTool);
 
                    // if we find the file
                    if (actualPathToTool != null)
                    {
                        // point to it
                        pathToTool = actualPathToTool;
                    }
                    else
                    {
                        // if we cannot find the file, we'll probably error out later on when
                        // we try to launch the tool; so do nothing for now
                    }
                }
            }
 
            return pathToTool;
        }
 
        /// <summary>
        /// Creates a temporary response file for the given command line arguments.
        /// We put as many command line arguments as we can into a response file to
        /// prevent the command line from getting too long. An overly long command
        /// line can cause the process creation to fail.
        /// </summary>
        /// <remarks>
        /// Command line arguments that cannot be put into response files, and which
        /// must appear on the command line, should not be passed to this method.
        /// </remarks>
        /// <param name="responseFileCommands">The command line arguments that need
        /// to go into the temporary response file.</param>
        /// <param name="responseFileSwitch">[out] The command line switch for using
        /// the temporary response file, or null if the response file is not needed.
        /// </param>
        /// <returns>The path to the temporary response file, or null if the response
        /// file is not needed.</returns>
        private string GetTemporaryResponseFile(string responseFileCommands, out string responseFileSwitch)
        {
            string responseFile = null;
            responseFileSwitch = null;
 
            // if this tool supports response files
            if (!string.IsNullOrEmpty(responseFileCommands))
            {
                // put all the parameters into a temporary response file so we don't
                // have to worry about how long the command-line is going to be
 
                // May throw IO-related exceptions
                responseFile = FileUtilities.GetTemporaryFileName(".rsp");
 
                // Use the encoding specified by the overridable ResponseFileEncoding property
                using (StreamWriter responseFileStream = FileUtilities.OpenWrite(responseFile, false, ResponseFileEncoding))
                {
                    responseFileStream.Write(ResponseFileEscape(responseFileCommands));
                }
 
                responseFileSwitch = GetResponseFileSwitch(responseFile);
            }
 
            return responseFile;
        }
 
        /// <summary>
        /// Initializes the information required to spawn the process executing the tool.
        /// </summary>
        /// <param name="pathToTool"></param>
        /// <param name="commandLineCommands"></param>
        /// <param name="responseFileSwitch"></param>
        /// <returns>The information required to start the process.</returns>
        protected virtual ProcessStartInfo GetProcessStartInfo(
            string pathToTool,
            string commandLineCommands,
            string responseFileSwitch)
        {
            // Build up the command line that will be spawned.
            string commandLine = commandLineCommands;
 
            if (!UseCommandProcessor)
            {
                if (!string.IsNullOrEmpty(responseFileSwitch))
                {
                    commandLine += " " + responseFileSwitch;
                }
            }
 
            // If the command is too long, it will most likely fail. The command line
            // arguments passed into any process cannot exceed 32768 characters, but
            // depending on the structure of the command (e.g. if it contains embedded
            // environment variables that will be expanded), longer commands might work,
            // or shorter commands might fail -- to play it safe, we warn at 32000.
            // NOTE: cmd.exe has a buffer limit of 8K, but we're not using cmd.exe here,
            // so we can go past 8K easily.
            if (commandLine.Length > 32000)
            {
                LogPrivate.LogWarningWithCodeFromResources("ToolTask.CommandTooLong", GetType().Name);
            }
 
            ProcessStartInfo startInfo = new ProcessStartInfo(pathToTool, commandLine);
            startInfo.CreateNoWindow = true;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardError = true;
            startInfo.RedirectStandardOutput = true;
            // ensure the redirected streams have the encoding we want
            startInfo.StandardErrorEncoding = StandardErrorEncoding;
            startInfo.StandardOutputEncoding = StandardOutputEncoding;
 
            if (NativeMethodsShared.IsWindows)
            {
                // Some applications such as xcopy.exe fail without error if there's no stdin stream.
                // We only do it under Windows, we get Pipe Broken IO exception on other systems if
                // the program terminates very fast.
                startInfo.RedirectStandardInput = true;
            }
 
            // Generally we won't set a working directory, and it will use the current directory
            string workingDirectory = GetWorkingDirectory();
            if (workingDirectory != null)
            {
                startInfo.WorkingDirectory = workingDirectory;
            }
 
            // Old style environment overrides
#pragma warning disable 0618 // obsolete
            Dictionary<string, string> envOverrides = EnvironmentOverride;
            if (envOverrides != null)
            {
                foreach (KeyValuePair<string, string> entry in envOverrides)
                {
                    startInfo.Environment[entry.Key] = entry.Value;
                }
#pragma warning restore 0618
            }
 
            // New style environment overrides
            if (_environmentVariablePairs != null)
            {
                foreach (KeyValuePair<string, string> variable in _environmentVariablePairs)
                {
                    startInfo.Environment[variable.Key] = variable.Value;
                }
            }
 
            return startInfo;
        }
 
        /// <summary>
        /// We expect tasks to override this method if they need information about the tool process or its process events during task execution.
        /// Implementation should make sure that the task is started in this method.
        /// Starts the process during task execution. 
        /// </summary>
        /// <param name="proc">Fully populated <see cref="Process"/> instance representing the tool process to be started.</param>
        /// <returns>A started process. This could be <paramref name="proc"/> or another <see cref="Process"/> instance.</returns>
        protected virtual Process StartToolProcess(Process proc)
        {
            proc.Start();
            return proc;
        }
 
        /// <summary>
        /// Writes out a temporary response file and shell-executes the tool requested.  Enables concurrent
        /// logging of the output of the tool.
        /// </summary>
        /// <param name="pathToTool">The computed path to tool executable on disk</param>
        /// <param name="responseFileCommands">Command line arguments that should go into a temporary response file</param>
        /// <param name="commandLineCommands">Command line arguments that should be passed to the tool executable directly</param>
        /// <returns>exit code from the tool - if errors were logged and the tool has an exit code of zero, then we sit it to -1</returns>
        protected virtual int ExecuteTool(
            string pathToTool,
            string responseFileCommands,
            string commandLineCommands)
        {
            if (!UseCommandProcessor)
            {
                LogPathToTool(ToolExe, pathToTool);
            }
 
            string responseFile = null;
            Process proc = null;
 
            _standardErrorData = new Queue();
            _standardOutputData = new Queue();
 
            _standardErrorDataAvailable = new ManualResetEvent(false);
            _standardOutputDataAvailable = new ManualResetEvent(false);
 
            _toolExited = new ManualResetEvent(false);
            _terminatedTool = false;
            _toolTimeoutExpired = new ManualResetEvent(false);
 
            _eventsDisposed = false;
 
            try
            {
                responseFile = GetTemporaryResponseFile(responseFileCommands, out string responseFileSwitch);
 
                // create/initialize the process to run the tool
                proc = new Process();
                proc.StartInfo = GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch);
 
                // turn on the Process.Exited event
                proc.EnableRaisingEvents = true;
                // sign up for the exit notification
                proc.Exited += ReceiveExitNotification;
 
                // turn on async stderr notifications
                proc.ErrorDataReceived += ReceiveStandardErrorData;
                // turn on async stdout notifications
                proc.OutputDataReceived += ReceiveStandardOutputData;
 
                // if we've got this far, we expect to get an exit code from the process. If we don't
                // get one from the process, we want to use an exit code value of -1.
                ExitCode = -1;
 
                // Start the process
                proc = StartToolProcess(proc);
 
                // Close the input stream. This is done to prevent commands from
                // blocking the build waiting for input from the user.
                if (NativeMethodsShared.IsWindows)
                {
                    proc.StandardInput.Dispose();
                }
 
                // Call user-provided hook for code that should execute immediately after the process starts
                this.ProcessStarted();
 
                // sign up for stderr callbacks
                proc.BeginErrorReadLine();
                // sign up for stdout callbacks
                proc.BeginOutputReadLine();
 
                // start the time-out timer
                _toolTimer = new Timer(ReceiveTimeoutNotification, null, Timeout, System.Threading.Timeout.Infinite /* no periodic timeouts */);
 
                // deal with the various notifications
                HandleToolNotifications(proc);
            }
            finally
            {
                // Delete the temp file used for the response file.
                if (responseFile != null)
                {
                    DeleteTempFile(responseFile);
                }
 
                // get the exit code and release the process handle
                if (proc != null)
                {
                    try
                    {
                        ExitCode = proc.ExitCode;
                    }
                    catch (InvalidOperationException)
                    {
                        // The process was never launched successfully.
                        // Leave the exit code at -1.
                    }
 
                    proc.Dispose();
                    proc = null;
                }
 
                // If the tool exited cleanly, but logged errors then assign a failing exit code (-1)
                if (ExitCode == 0 && HasLoggedErrors)
                {
                    ExitCode = -1;
                }
 
                // release all the OS resources
                // setting a bool to make sure tardy notification threads
                // don't try to set the event after this point
                lock (_eventCloseLock)
                {
                    _eventsDisposed = true;
                    _standardErrorDataAvailable.Dispose();
                    _standardOutputDataAvailable.Dispose();
 
                    _toolExited.Dispose();
                    _toolTimeoutExpired.Dispose();
 
                    _toolTimer?.Dispose();
                }
            }
 
            return ExitCode;
        }
 
        /// <summary>
        /// Cancels the process executing the task by asking it to close nicely, then after a short period, forcing termination.
        /// </summary>
        public virtual void Cancel() => ToolCanceled.Set();
 
        /// <summary>
        /// Delete temporary file. If the delete fails for some reason (e.g. file locked by anti-virus) then
        /// the call will not throw an exception. Instead a warning will be logged, but the build will not fail.
        /// </summary>
        /// <param name="fileName">File to delete</param>
        protected void DeleteTempFile(string fileName)
        {
            if (s_preserveTempFiles)
            {
                Log.LogMessageFromText($"Preserving temporary file '{fileName}'", MessageImportance.Low);
                return;
            }
 
            try
            {
                File.Delete(fileName);
            }
            catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
            {
                // Warn only -- occasionally temp files fail to delete because of virus checkers; we
                // don't want the build to fail in such cases
                LogShared.LogWarningWithCodeFromResources("Shared.FailedDeletingTempFile", fileName, e.Message);
            }
        }
 
        /// <summary>
        /// Handles all the notifications sent while the tool is executing. The
        /// notifications can be for tool output, tool time-out, or tool completion.
        /// </summary>
        /// <remarks>
        /// The slightly convoluted use of the async stderr/stdout streams of the
        /// Process class is necessary because we want to log all our messages from
        /// the main thread, instead of from a worker or callback thread.
        /// </remarks>
        /// <param name="proc"></param>
        private void HandleToolNotifications(Process proc)
        {
            // NOTE: the ordering of this array is deliberate -- if multiple
            // notifications are sent simultaneously, we want to handle them
            // in the order specified by the array, so that we can observe the
            // following rules:
            // 1) if a tool times-out we want to abort it immediately regardless
            //    of whether its stderr/stdout queues are empty
            // 2) if a tool exits, we first want to flush its stderr/stdout queues
            // 3) if a tool exits and times-out at the same time, we want to let
            //    it exit gracefully
            WaitHandle[] notifications =
            {
                _toolTimeoutExpired,
                ToolCanceled,
                _standardErrorDataAvailable,
                _standardOutputDataAvailable,
                _toolExited
            };
 
            bool isToolRunning = true;
 
            if (YieldDuringToolExecution)
            {
                BuildEngine3.Yield();
            }
 
            try
            {
                while (isToolRunning)
                {
                    // wait for something to happen -- we block the main thread here
                    // because we don't want to uselessly consume CPU cycles; in theory
                    // we could poll the stdout and stderr queues, but polling is not
                    // good for performance, and so we use ManualResetEvents to wake up
                    // the main thread only when necessary
                    // NOTE: the return value from WaitAny() is the array index of the
                    // notification that was sent; if multiple notifications are sent
                    // simultaneously, the return value is the index of the notification
                    // with the smallest index value of all the sent notifications
                    int notificationIndex = WaitHandle.WaitAny(notifications);
 
                    switch (notificationIndex)
                    {
                        // tool timed-out
                        case 0:
                        // tool was canceled
                        case 1:
                            TerminateToolProcess(proc, notificationIndex == 1);
                            _terminatedTool = true;
                            isToolRunning = false;
                            break;
                        // tool wrote to stderr (and maybe stdout also)
                        case 2:
                            LogMessagesFromStandardError();
                            // if stderr and stdout notifications were sent simultaneously, we
                            // must alternate between the queues, and not starve the stdout queue
                            LogMessagesFromStandardOutput();
                            break;
 
                        // tool wrote to stdout
                        case 3:
                            LogMessagesFromStandardOutput();
                            break;
 
                        // tool exited
                        case 4:
                            // We need to do this to guarantee the stderr/stdout streams
                            // are empty -- there seems to be no other way of telling when the
                            // process is done sending its async stderr/stdout notifications; why
                            // is the Process class sending the exit notification prematurely?
                            WaitForProcessExit(proc);
 
                            // flush the stderr and stdout queues to clear out the data placed
                            // in them while we were waiting for the process to exit
                            LogMessagesFromStandardError();
                            LogMessagesFromStandardOutput();
                            isToolRunning = false;
                            break;
 
                        default:
                            ErrorUtilities.ThrowInternalError("Unknown tool notification.");
                            break;
                    }
                }
            }
            finally
            {
                if (YieldDuringToolExecution)
                {
                    BuildEngine3.Reacquire();
                }
            }
        }
 
        /// <summary>
        /// Kills the given process that is executing the tool, because the tool's
        /// time-out period expired.
        /// </summary>
        private void KillToolProcessOnTimeout(Process proc, bool isBeingCancelled)
        {
            // kill the process if it's not finished yet
            if (!proc.HasExited)
            {
                string processName;
                try
                {
                    processName = proc.ProcessName;
                }
                catch (InvalidOperationException)
                {
                    // Process exited in the small interval since we checked HasExited
                    return;
                }
 
                if (!isBeingCancelled)
                {
                    ErrorUtilities.VerifyThrow(Timeout != System.Threading.Timeout.Infinite,
                        "A time-out value must have been specified or the task must be cancelled.");
 
                    LogShared.LogWarningWithCodeFromResources("Shared.KillingProcess", processName, Timeout);
                }
                else
                {
                    LogShared.LogWarningWithCodeFromResources("Shared.KillingProcessByCancellation", processName);
                }
 
                int timeout = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) && TaskProcessTerminationTimeout >= -1 ? TaskProcessTerminationTimeout : 5000;
                string timeoutFromEnvironment = Environment.GetEnvironmentVariable("MSBUILDTOOLTASKCANCELPROCESSWAITTIMEOUT");
                if (timeoutFromEnvironment != null)
                {
                    if (int.TryParse(timeoutFromEnvironment, out int result) && result >= 0)
                    {
                        timeout = result;
                    }
                }
                proc.KillTree(timeout);
            }
        }
 
        /// <summary>
        /// Kills the specified process
        /// </summary>
        private void TerminateToolProcess(Process proc, bool isBeingCancelled)
        {
            if (proc != null)
            {
                if (proc.HasExited)
                {
                    return;
                }
 
                if (isBeingCancelled)
                {
                    try
                    {
                        proc.CancelOutputRead();
                        proc.CancelErrorRead();
                    }
                    catch (InvalidOperationException)
                    {
                        // The task possibly never started.
                    }
                }
 
                KillToolProcessOnTimeout(proc, isBeingCancelled);
            }
        }
 
        /// <summary>
        /// Confirms that the given process has really and truly exited. If the
        /// process is still finishing up, this method waits until it is done.
        /// </summary>
        /// <remarks>
        /// This method is a hack, but it needs to be called after both
        /// Process.WaitForExit() and Process.Kill().
        /// </remarks>
        /// <param name="proc"></param>
        private static void WaitForProcessExit(Process proc)
        {
            proc.WaitForExit();
 
            // Process.WaitForExit() may return prematurely. We need to check to be sure.
            while (!proc.HasExited)
            {
                Thread.Sleep(50);
            }
        }
 
        /// <summary>
        /// Logs all the messages that the tool wrote to stderr. The messages
        /// are read out of the stderr data queue.
        /// </summary>
        private void LogMessagesFromStandardError()
            => LogMessagesFromStandardErrorOrOutput(_standardErrorData, _standardErrorDataAvailable, _standardErrorImportanceToUse, StandardOutputOrErrorQueueType.StandardError);
 
        /// <summary>
        /// Logs all the messages that the tool wrote to stdout. The messages
        /// are read out of the stdout data queue.
        /// </summary>
        private void LogMessagesFromStandardOutput()
            => LogMessagesFromStandardErrorOrOutput(_standardOutputData, _standardOutputDataAvailable, _standardOutputImportanceToUse, StandardOutputOrErrorQueueType.StandardOutput);
 
        /// <summary>
        /// Logs all the messages that the tool wrote to either stderr or stdout.
        /// The messages are read out of the given data queue. This method is a
        /// helper for the <see cref="LogMessagesFromStandardError"/>() and <see
        /// cref="LogMessagesFromStandardOutput"/>() methods.
        /// </summary>
        /// <param name="dataQueue"></param>
        /// <param name="dataAvailableSignal"></param>
        /// <param name="messageImportance"></param>
        /// <param name="queueType"></param>
        private void LogMessagesFromStandardErrorOrOutput(
            Queue dataQueue,
            ManualResetEvent dataAvailableSignal,
            MessageImportance messageImportance,
            StandardOutputOrErrorQueueType queueType)
        {
            ErrorUtilities.VerifyThrow(dataQueue != null,
                "The data queue must be available.");
 
            // synchronize access to the queue -- this is a producer-consumer problem
            // NOTE: the synchronization problem here is actually not about the queue
            // at all -- if we only cared about reading from and writing to the queue,
            // we could use a synchronized wrapper around the queue, and things would
            // work perfectly; the synchronization problem here is actually around the
            // ManualResetEvent -- while a ManualResetEvent itself is a thread-safe
            // type, the information we infer from the state of a ManualResetEvent is
            // not thread-safe; because a ManualResetEvent does not have a ref count,
            // we cannot safely set (or reset) it outside of a synchronization block;
            // therefore instead of using synchronized queue wrappers, we just lock the
            // entire queue, empty it, and reset the ManualResetEvent before releasing
            // the lock; this also allows proper alternation between the stderr and
            // stdout queues -- otherwise we would continuously read from one queue and
            // starve the other; locking out the producer allows the consumer to
            // alternate between the queues
            lock (dataQueue.SyncRoot)
            {
                while (dataQueue.Count > 0)
                {
                    string errorOrOutMessage = dataQueue.Dequeue() as string;
                    if (!LogStandardErrorAsError || queueType == StandardOutputOrErrorQueueType.StandardOutput)
                    {
                        LogEventsFromTextOutput(errorOrOutMessage, messageImportance);
                    }
                    else if (LogStandardErrorAsError && queueType == StandardOutputOrErrorQueueType.StandardError)
                    {
                        Log.LogError(errorOrOutMessage);
                    }
                }
 
                ErrorUtilities.VerifyThrow(dataAvailableSignal != null,
                    "The signalling event must be available.");
 
                // the queue is empty, so reset the notification
                // NOTE: intentionally, do the reset inside the lock, because
                // ManualResetEvents don't have ref counts, and we want to make
                // sure we don't reset the notification just after the producer
                // signals it
                dataAvailableSignal.Reset();
            }
        }
 
        /// <summary>
        /// Calls a method on the TaskLoggingHelper to parse a single line of text to
        /// see if there are any errors or warnings in canonical format.  This can
        /// be overridden by the derived class if necessary.
        /// </summary>
        /// <param name="singleLine"></param>
        /// <param name="messageImportance"></param>
        protected virtual void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance) => Log.LogMessageFromText(singleLine, messageImportance);
 
        /// <summary>
        /// Signals when the tool times-out. The tool timer calls this method
        /// when the time-out period on the tool expires.
        /// </summary>
        /// <remarks>This method is used as a System.Threading.TimerCallback delegate.</remarks>
        /// <param name="unused"></param>
        private void ReceiveTimeoutNotification(object unused)
        {
            ErrorUtilities.VerifyThrow(_toolTimeoutExpired != null,
                "The signalling event for tool time-out must be available.");
            lock (_eventCloseLock)
            {
                if (!_eventsDisposed)
                {
                    _toolTimeoutExpired.Set();
                }
            }
        }
 
        /// <summary>
        /// Signals when the tool exits. The Process object executing the tool
        /// calls this method when the tool exits.
        /// </summary>
        /// <remarks>This method is used as a System.EventHandler delegate.</remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ReceiveExitNotification(object sender, EventArgs e)
        {
            ErrorUtilities.VerifyThrow(_toolExited != null,
                "The signalling event for tool exit must be available.");
 
            lock (_eventCloseLock)
            {
                if (!_eventsDisposed)
                {
                    _toolExited.Set();
                }
            }
        }
 
        /// <summary>
        /// Queues up the output from the stderr stream of the process executing
        /// the tool, and signals the availability of the data. The Process object
        /// executing the tool calls this method for every line of text that the
        /// tool writes to stderr.
        /// </summary>
        /// <remarks>This method is used as a System.Diagnostics.DataReceivedEventHandler delegate.</remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ReceiveStandardErrorData(object sender, DataReceivedEventArgs e) => ReceiveStandardErrorOrOutputData(e, _standardErrorData, _standardErrorDataAvailable);
 
        /// <summary>
        /// Queues up the output from the stdout stream of the process executing
        /// the tool, and signals the availability of the data. The Process object
        /// executing the tool calls this method for every line of text that the
        /// tool writes to stdout.
        /// </summary>
        /// <remarks>This method is used as a System.Diagnostics.DataReceivedEventHandler delegate.</remarks>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ReceiveStandardOutputData(object sender, DataReceivedEventArgs e) => ReceiveStandardErrorOrOutputData(e, _standardOutputData, _standardOutputDataAvailable);
 
        /// <summary>
        /// Queues up the output from either the stderr or stdout stream of the
        /// process executing the tool, and signals the availability of the data.
        /// This method is a helper for the <see cref="ReceiveStandardErrorData"/>()
        /// and <see cref="ReceiveStandardOutputData"/>() methods.
        /// </summary>
        /// <param name="e"></param>
        /// <param name="dataQueue"></param>
        /// <param name="dataAvailableSignal"></param>
        private void ReceiveStandardErrorOrOutputData(DataReceivedEventArgs e, Queue dataQueue, ManualResetEvent dataAvailableSignal)
        {
            // NOTE: don't ignore empty string, because we need to log that
            if (e.Data != null)
            {
                ErrorUtilities.VerifyThrow(dataQueue != null,
                    "The data queue must be available.");
 
                // synchronize access to the queue -- this is a producer-consumer problem
                // NOTE: we lock the entire queue instead of using synchronized queue
                // wrappers, because ManualResetEvents don't have ref counts, and it's
                // difficult to discretely signal the availability of each instance of
                // data in the queue -- so instead we let the consumer lock and empty
                // the queue and reset the ManualResetEvent, before we add more data
                // into the queue, and signal the ManualResetEvent again
                lock (dataQueue.SyncRoot)
                {
                    dataQueue.Enqueue(e.Data);
 
                    ErrorUtilities.VerifyThrow(dataAvailableSignal != null,
                        "The signalling event must be available.");
 
                    // signal the availability of data
                    // NOTE: intentionally, do the signalling inside the lock, because
                    // ManualResetEvents don't have ref counts, and we want to make sure
                    // we don't signal the notification just before the consumer resets it
                    lock (_eventCloseLock)
                    {
                        if (!_eventsDisposed)
                        {
                            dataAvailableSignal.Set();
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Assign the importances that will be used for stdout/stderr logging of messages from this tool task.
        /// This takes into account (1 is highest precedence):
        /// 1. the override value supplied as a task parameter.
        /// 2. those overridden by any derived class and
        /// 3. the defaults given by tooltask
        /// </summary>
        private bool AssignStandardStreamLoggingImportance()
        {
            // Gather the importance for the Standard Error stream:
            if (string.IsNullOrEmpty(StandardErrorImportance))
            {
                // If we have no task parameter override then ask the task for its default
                _standardErrorImportanceToUse = StandardErrorLoggingImportance;
            }
            else
            {
                try
                {
                    // Parse the raw importance string into a strongly typed enumeration.
                    _standardErrorImportanceToUse = (MessageImportance)Enum.Parse(typeof(MessageImportance), StandardErrorImportance, true /* case-insensitive */);
                }
                catch (ArgumentException)
                {
                    Log.LogErrorWithCodeFromResources("Message.InvalidImportance", StandardErrorImportance);
                    return false;
                }
            }
 
            // Gather the importance for the Standard Output stream:
            if (string.IsNullOrEmpty(StandardOutputImportance))
            {
                // If we have no task parameter override then ask the task for its default
                _standardOutputImportanceToUse = StandardOutputLoggingImportance;
            }
            else
            {
                try
                {
                    // Parse the raw importance string into a strongly typed enumeration.
                    _standardOutputImportanceToUse = (MessageImportance)Enum.Parse(typeof(MessageImportance), StandardOutputImportance, true /* case-insensitive */);
                }
                catch (ArgumentException)
                {
                    Log.LogErrorWithCodeFromResources("Message.InvalidImportance", StandardOutputImportance);
                    return false;
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Looks for the given file in the system path i.e. all locations in the %PATH% environment variable.
        /// </summary>
        /// <param name="filename"></param>
        /// <returns>The location of the file, or null if file not found.</returns>
        internal static string FindOnPath(string filename)
        {
            // Get path from the environment and split path separator
            return Environment.GetEnvironmentVariable("PATH")?
                .Split(MSBuildConstants.PathSeparatorChar)?
                .Where(path =>
                {
                    try
                    {
                        // The PATH can contain anything, including bad characters
                        return FileSystems.Default.DirectoryExists(path);
                    }
                    catch (Exception)
                    {
                        return false;
                    }
                })
                .Select(folderPath => Path.Combine(folderPath, filename))
                .FirstOrDefault(fullPath => !string.IsNullOrEmpty(fullPath) && FileSystems.Default.FileExists(fullPath));
        }
 
        #endregion
 
        #region ITask Members
 
        /// <summary>
        /// This method invokes the tool with the given parameters.
        /// </summary>
        /// <returns>true, if task executes successfully</returns>
        public override bool Execute()
        {
            // Let the tool validate its parameters.
            if (!ValidateParameters())
            {
                // The ToolTask is responsible for logging useful information about what was wrong with the
                // parameters; if it didn't, at least emit a generic message.
                if (!Log.HasLoggedErrors)
                {
                    LogPrivate.LogErrorWithCodeFromResources("ToolTask.ValidateParametersFailed", this.GetType().FullName);
                }
                return false;
            }
 
            if (EnvironmentVariables != null)
            {
                _environmentVariablePairs = new List<KeyValuePair<string, string>>(EnvironmentVariables.Length);
 
                foreach (string entry in EnvironmentVariables)
                {
                    string[] nameValuePair = entry.Split(s_equalsSplitter, 2);
 
                    if (nameValuePair.Length == 1 || (nameValuePair.Length == 2 && nameValuePair[0].Length == 0))
                    {
                        LogPrivate.LogErrorWithCodeFromResources("ToolTask.InvalidEnvironmentParameter", nameValuePair[0]);
                        return false;
                    }
 
                    _environmentVariablePairs.Add(new KeyValuePair<string, string>(nameValuePair[0], nameValuePair[1]));
                }
            }
 
            // Assign standard stream logging importances
            if (!AssignStandardStreamLoggingImportance())
            {
                return false;
            }
 
            try
            {
                if (SkipTaskExecution())
                {
                    // the task has said there's no command-line that we need to run, so
                    // return true to indicate this task completed successfully (without
                    // doing any actual work).
                    return true;
                }
                else if (canBeIncremental && FailIfNotIncremental)
                {
                    LogPrivate.LogErrorWithCodeFromResources("ToolTask.NotUpToDate");
                    return false;
                }
 
                string commandLineCommands = GenerateCommandLineCommands();
                // If there are response file commands, then we need a response file later.
                string batchFileContents = commandLineCommands;
                string responseFileCommands = GenerateResponseFileCommands();
                bool runningOnWindows = NativeMethodsShared.IsWindows;
 
                if (UseCommandProcessor)
                {
                    if (runningOnWindows) // we are Windows
                    {
                        ToolExe = "cmd.exe";
                        // Generate the temporary batch file
                        // May throw IO-related exceptions
                        _temporaryBatchFile = FileUtilities.GetTemporaryFile(".cmd");
                    }
                    else
                    {
                        ToolExe = "/bin/sh";
                        // Generate the temporary batch file
                        // May throw IO-related exceptions
                        _temporaryBatchFile = FileUtilities.GetTemporaryFile(".sh");
                    }
 
                    if (!runningOnWindows)
                    {
                        // Use sh rather than bash, as not all 'nix systems necessarily have Bash installed
                        File.AppendAllText(_temporaryBatchFile, "#!/bin/sh\n"); // first line for UNIX is ANSI
                        // This is a hack..!
                        File.AppendAllText(_temporaryBatchFile, AdjustCommandsForOperatingSystem(commandLineCommands), EncodingUtilities.CurrentSystemOemEncoding);
 
                        commandLineCommands = $"\"{_temporaryBatchFile}\"";
                    }
                    else
                    {
                        Encoding encoding;
 
                        if (Traits.Instance.EscapeHatches.AvoidUnicodeWhenWritingToolTaskBatch)
                        {
                            encoding = EncodingUtilities.CurrentSystemOemEncoding;
                        }
                        else
                        {
                            encoding = EncodingUtilities.BatchFileEncoding(commandLineCommands + _temporaryBatchFile, UseUtf8Encoding);
 
                            if (encoding.CodePage != EncodingUtilities.CurrentSystemOemEncoding.CodePage)
                            {
                                // cmd.exe reads the first line in the console CP,
                                // which for a new console (as here) is OEMCP
                                // this string should ideally always be ASCII
                                // and the same in any OEMCP.
                                File.AppendAllText(_temporaryBatchFile,
                                                   $@"%SystemRoot%\System32\chcp.com {encoding.CodePage}>nul{Environment.NewLine}",
                                                   EncodingUtilities.CurrentSystemOemEncoding);
                            }
                        }
 
                        File.AppendAllText(_temporaryBatchFile, commandLineCommands, encoding);
                        if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10))
                        {
                            _encoding = encoding;
                        }
 
                        string batchFileForCommandLine = _temporaryBatchFile;
 
                        // If for some reason the path has a & character and a space in it
                        // then get the short path of the temp path, which should not have spaces in it
                        // and then escape the &
                        if (batchFileForCommandLine.Contains("&") && !batchFileForCommandLine.Contains("^&"))
                        {
                            batchFileForCommandLine = NativeMethodsShared.GetShortFilePath(batchFileForCommandLine);
                            batchFileForCommandLine = batchFileForCommandLine.Replace("&", "^&");
                        }
 
                        // /D: Do not load AutoRun configuration from the registry (perf)
                        commandLineCommands = $"{(Traits.Instance.EscapeHatches.UseAutoRunWhenLaunchingProcessUnderCmd ? string.Empty : "/D ")}/C \"{batchFileForCommandLine}\"";
 
                        if (EchoOff)
                        {
                            commandLineCommands = "/Q " + commandLineCommands;
                        }
                    }
                }
 
                // ensure the command line arguments string is not null
                if (string.IsNullOrEmpty(commandLineCommands))
                {
                    commandLineCommands = string.Empty;
                }
                // add a leading space to the command line arguments (if any) to
                // separate them from the tool path
                else
                {
                    commandLineCommands = " " + commandLineCommands;
                }
 
                // Initialize the host object.  At this point, the task may elect
                // to not proceed.  Compiler tasks do this for purposes of up-to-date
                // checking in the IDE.
                HostObjectInitializationStatus nextAction = InitializeHostObject();
                if (nextAction == HostObjectInitializationStatus.NoActionReturnSuccess)
                {
                    return true;
                }
                else if (nextAction == HostObjectInitializationStatus.NoActionReturnFailure)
                {
                    ExitCode = 1;
                    return HandleTaskExecutionErrors();
                }
 
                string pathToTool = ComputePathToTool();
                if (pathToTool == null)
                {
                    // An appropriate error should have been logged already.
                    return false;
                }
 
                // Log the environment. We do this up here,
                // rather than later where the environment is set,
                // so that it appears before the command line is logged.
                bool alreadyLoggedEnvironmentHeader = false;
 
                // Old style environment overrides
#pragma warning disable 0618 // obsolete
                Dictionary<string, string> envOverrides = EnvironmentOverride;
                if (envOverrides != null)
                {
                    foreach (KeyValuePair<string, string> entry in envOverrides)
                    {
                        alreadyLoggedEnvironmentHeader = LogEnvironmentVariable(alreadyLoggedEnvironmentHeader, entry.Key, entry.Value);
                    }
#pragma warning restore 0618
                }
 
                // New style environment overrides
                if (_environmentVariablePairs != null)
                {
                    foreach (KeyValuePair<string, string> variable in _environmentVariablePairs)
                    {
                        alreadyLoggedEnvironmentHeader = LogEnvironmentVariable(alreadyLoggedEnvironmentHeader, variable.Key, variable.Value);
                    }
                }
 
                commandLineCommands = AdjustCommandsForOperatingSystem(commandLineCommands);
                responseFileCommands = AdjustCommandsForOperatingSystem(responseFileCommands);
 
                if (UseCommandProcessor)
                {
                    // Log that we are about to invoke the specified command.
                    LogToolCommand(pathToTool + commandLineCommands);
                    LogToolCommand(batchFileContents);
                }
                else
                {
                    // Log that we are about to invoke the specified command.
                    LogToolCommand(pathToTool + commandLineCommands + " " + responseFileCommands);
                }
                ExitCode = 0;
 
                if (nextAction == HostObjectInitializationStatus.UseHostObjectToExecute)
                {
                    // The hosting IDE passed in a host object to this task.  Give the task
                    // a chance to call this host object to do the actual work.
                    try
                    {
                        if (!CallHostObjectToExecute())
                        {
                            ExitCode = 1;
                        }
                    }
                    catch (Exception e)
                    {
                        LogPrivate.LogErrorFromException(e);
                        return false;
                    }
                }
                else
                {
                    ErrorUtilities.VerifyThrow(nextAction == HostObjectInitializationStatus.UseAlternateToolToExecute,
                        "Invalid return status");
 
                    // No host object was provided, or at least not one that supports all of the
                    // switches/parameters we need.  So shell out to the command-line tool.
                    ExitCode = ExecuteTool(pathToTool, responseFileCommands, commandLineCommands);
                }
 
                // Raise a comment event to notify that the process completed
                if (_terminatedTool)
                {
                    return false;
                }
                else if (ExitCode != 0)
                {
                    return HandleTaskExecutionErrors();
                }
                else
                {
                    return true;
                }
            }
            catch (ArgumentException e)
            {
                if (!_terminatedTool)
                {
                    LogPrivate.LogErrorWithCodeFromResources("General.InvalidToolSwitch", ToolExe, e.ToString());
                }
                return false;
            }
            catch (Exception e) when (e is Win32Exception || e is IOException || e is UnauthorizedAccessException)
            {
                if (!_terminatedTool)
                {
                    LogPrivate.LogErrorWithCodeFromResources("ToolTask.CouldNotStartToolExecutable", ToolExe, e.ToString());
                }
                return false;
            }
            finally
            {
                // Clean up after ourselves.
                if (_temporaryBatchFile != null && FileSystems.Default.FileExists(_temporaryBatchFile))
                {
                    DeleteTempFile(_temporaryBatchFile);
                }
            }
        } // Execute()
 
        /// <summary>
        /// Replace backslashes with OS-specific path separators,
        /// except when likely that the backslash is intentional.
        /// </summary>
        /// <remarks>
        /// Not a static method so that an implementation can
        /// override with more-specific knowledge of what backslashes
        /// are likely to be correct.
        /// </remarks>
        protected virtual string AdjustCommandsForOperatingSystem(string input)
        {
            if (NativeMethodsShared.IsWindows)
            {
                return input;
            }
 
            StringBuilder sb = new StringBuilder(input);
 
            int length = sb.Length;
 
            for (int i = 0; i < length; i++)
            {
                // Backslashes must be swapped, because we don't
                // know what inputs are paths or path fragments.
                // But it's a common pattern to have backslash-escaped
                // quotes inside quotes--especially for VB that has a default like
                //
                // /define:"CONFIG=\"Debug\",DEBUG=-1,TRACE=-1,_MyType=\"Console\",PLATFORM=\"AnyCPU\""
                //
                // So don't replace a backslash immediately
                // followed by a quote.
                if (sb[i] == '\\' && (i == length - 1 || sb[i + 1] != '"'))
                {
                    sb[i] = Path.DirectorySeparatorChar;
                }
            }
 
            return sb.ToString();
        }
 
        /// <summary>
        /// Log a single environment variable that's about to be applied to the tool
        /// </summary>
        private bool LogEnvironmentVariable(bool alreadyLoggedEnvironmentHeader, string key, string value)
        {
            if (!alreadyLoggedEnvironmentHeader)
            {
                LogPrivate.LogMessageFromResources(MessageImportance.Low, "ToolTask.EnvironmentVariableHeader");
                alreadyLoggedEnvironmentHeader = true;
            }
 
            Log.LogMessage(MessageImportance.Low, "  {0}={1}", key, value);
 
            return alreadyLoggedEnvironmentHeader;
        }
 
        #endregion
 
        #region Member data
 
        /// <summary>
        /// An object to hold the event shutdown lock
        /// </summary>
        private readonly object _eventCloseLock = new object();
 
        /// <summary>
        /// Splitter for environment variables
        /// </summary>
        private static readonly char[] s_equalsSplitter = MSBuildConstants.EqualsChar;
 
        /// <summary>
        /// The actual importance at which standard out messages will be logged
        /// </summary>
        private MessageImportance _standardOutputImportanceToUse = MessageImportance.Low;
 
        /// <summary>
        /// The actual importance at which standard error messages will be logged
        /// </summary>
        private MessageImportance _standardErrorImportanceToUse = MessageImportance.Normal;
 
        /// <summary>
        /// Holds the stderr output from the tool.
        /// </summary>
        /// <remarks>This collection is NOT thread-safe.</remarks>
        private Queue _standardErrorData;
 
        /// <summary>
        /// Holds the stdout output from the tool.
        /// </summary>
        /// <remarks>This collection is NOT thread-safe.</remarks>
        private Queue _standardOutputData;
 
        /// <summary>
        /// Used for signalling when the tool writes to stderr.
        /// </summary>
        private ManualResetEvent _standardErrorDataAvailable;
 
        /// <summary>
        /// Used for signalling when the tool writes to stdout.
        /// </summary>
        private ManualResetEvent _standardOutputDataAvailable;
 
        /// <summary>
        /// Used for signalling when the tool exits.
        /// </summary>
        private ManualResetEvent _toolExited;
 
        /// <summary>
        /// Set to true if the tool process was terminated,
        /// either because the timeout was reached or it was canceled.
        /// </summary>
        private bool _terminatedTool;
 
        /// <summary>
        /// Used for signalling when the tool times-out.
        /// </summary>
        private ManualResetEvent _toolTimeoutExpired;
 
        /// <summary>
        /// Used for timing-out the tool.
        /// </summary>
        private Timer _toolTimer;
 
        /// <summary>
        /// Used to support overriding the toolExe name.
        /// </summary>
        private string _toolExe;
 
        /// <summary>
        /// Set when the events are about to be disposed, so that tardy
        /// calls on the event handlers don't try to reset a disposed event
        /// </summary>
        private bool _eventsDisposed;
 
        /// <summary>
        /// List of name, value pairs to be passed to the spawned tool's environment.
        /// May be null.
        /// </summary>
        private List<KeyValuePair<string, string>> _environmentVariablePairs;
 
        /// <summary>
        /// Enumeration which indicates what kind of queue is being passed
        /// </summary>
        private enum StandardOutputOrErrorQueueType
        {
            StandardError = 0,
            StandardOutput = 1
        }
 
        #endregion
    }
}