File: Utilities\ProcessRunner.cs
Web Access
Project: ..\..\..\src\BuiltInTools\dotnet-format\dotnet-format.csproj (dotnet-format)
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.
 
using System.Collections.ObjectModel;
using System.Diagnostics;
 
namespace Microsoft.CodeAnalysis.Tools.Utilities
{
    public readonly struct ProcessResult
    {
        public Process Process { get; }
        public int ExitCode { get; }
        public ReadOnlyCollection<string> OutputLines { get; }
        public ReadOnlyCollection<string> ErrorLines { get; }
 
        public ProcessResult(Process process, int exitCode, ReadOnlyCollection<string> outputLines, ReadOnlyCollection<string> errorLines)
        {
            Process = process;
            ExitCode = exitCode;
            OutputLines = outputLines;
            ErrorLines = errorLines;
        }
    }
 
    public readonly struct ProcessInfo
    {
        public Process Process { get; }
        public ProcessStartInfo StartInfo { get; }
        public Task<ProcessResult> Result { get; }
 
        public int Id => Process.Id;
 
        public ProcessInfo(Process process, ProcessStartInfo startInfo, Task<ProcessResult> result)
        {
            Process = process;
            StartInfo = startInfo;
            Result = result;
        }
    }
 
    public static class ProcessRunner
    {
        public static void OpenFile(string file)
        {
            if (File.Exists(file))
            {
                Process.Start(file);
            }
        }
 
        public static ProcessInfo CreateProcess(
            string executable,
            string arguments,
            bool lowPriority = false,
            string? workingDirectory = null,
            bool captureOutput = false,
            bool displayWindow = true,
            Dictionary<string, string>? environmentVariables = null,
            Action<Process>? onProcessStartHandler = null,
            CancellationToken cancellationToken = default) =>
            CreateProcess(
                CreateProcessStartInfo(executable, arguments, workingDirectory, captureOutput, displayWindow, environmentVariables),
                lowPriority: lowPriority,
                onProcessStartHandler: onProcessStartHandler,
                cancellationToken: cancellationToken);
 
        public static ProcessInfo CreateProcess(
            ProcessStartInfo processStartInfo,
            bool lowPriority = false,
            Action<Process>? onProcessStartHandler = null,
            CancellationToken cancellationToken = default)
        {
            var redirectInitiated = new ManualResetEventSlim();
 
            var errorLines = new List<string>();
            var outputLines = new List<string>();
            var process = new Process();
            var tcs = new TaskCompletionSource<ProcessResult>();
 
            process.EnableRaisingEvents = true;
            process.StartInfo = processStartInfo;
 
            process.OutputDataReceived += (s, e) =>
                {
                    if (e.Data != null)
                    {
                        outputLines.Add(e.Data);
                    }
                };
 
            process.ErrorDataReceived += (s, e) =>
                {
                    if (e.Data != null)
                    {
                        errorLines.Add(e.Data);
                    }
                };
 
            process.Exited += (s, e) =>
                {
                    // We must call WaitForExit to make sure we've received all OutputDataReceived/ErrorDataReceived calls
                    // or else we'll be returning a list we're still modifying. For paranoia, we'll start a task here rather
                    // than enter right back into the Process type and start a wait which isn't guaranteed to be safe.
                    Task.Run(() =>
                    {
                        // WaitForExit will only wait for the process to finish redirecting its output/error if we call
                        // BeginOutputReadLine/BeginErrorReadLine prior to calling WaitForExit. If we do not wait for these
                        // methods to be called, its possible to return before we get any data from the process.
                        redirectInitiated.Wait();
                        redirectInitiated.Dispose();
                        redirectInitiated = null;
 
                        process.WaitForExit();
                        var result = new ProcessResult(
                            process,
                            process.ExitCode,
                            new ReadOnlyCollection<string>(outputLines),
                            new ReadOnlyCollection<string>(errorLines));
                        tcs.TrySetResult(result);
                    });
                };
 
            _ = cancellationToken.Register(() =>
                {
                    if (tcs.TrySetCanceled())
                    {
                        // If the underlying process is still running, we should kill it
                        if (!process.HasExited)
                        {
                            try
                            {
                                // This will cause Exited to be fired if it already hasn't, ensuring redirectInitiated
                                // is still disposed even on the cancellation path.
                                process.Kill();
                            }
                            catch (InvalidOperationException)
                            {
                                // Ignore, since the process is already dead
                            }
                        }
                    }
                });
 
            process.Start();
            onProcessStartHandler?.Invoke(process);
 
            if (lowPriority)
            {
                process.PriorityClass = ProcessPriorityClass.BelowNormal;
            }
 
            if (processStartInfo.RedirectStandardOutput)
            {
                process.BeginOutputReadLine();
            }
 
            if (processStartInfo.RedirectStandardError)
            {
                process.BeginErrorReadLine();
            }
 
            redirectInitiated.Set();
 
            return new ProcessInfo(process, processStartInfo, tcs.Task);
        }
 
        public static ProcessStartInfo CreateProcessStartInfo(
            string executable,
            string arguments,
            string? workingDirectory = null,
            bool captureOutput = false,
            bool displayWindow = true,
            Dictionary<string, string>? environmentVariables = null)
        {
            var processStartInfo = new ProcessStartInfo(executable, arguments);
 
            if (!string.IsNullOrEmpty(workingDirectory))
            {
                processStartInfo.WorkingDirectory = workingDirectory;
            }
 
            if (environmentVariables != null)
            {
                foreach (var pair in environmentVariables)
                {
                    processStartInfo.EnvironmentVariables[pair.Key] = pair.Value;
                }
            }
 
            if (captureOutput)
            {
                processStartInfo.UseShellExecute = false;
                processStartInfo.RedirectStandardOutput = true;
                processStartInfo.RedirectStandardError = true;
            }
            else
            {
                processStartInfo.CreateNoWindow = !displayWindow;
                processStartInfo.UseShellExecute = displayWindow;
            }
 
            return processStartInfo;
        }
    }
}