File: ProcessRunner.cs
Web Access
Project: src\src\Tools\Source\RunTests\RunTests.csproj (RunTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
 
namespace RunTests
{
    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,
            Action<DataReceivedEventArgs>? onOutputDataReceived = null,
            CancellationToken cancellationToken = default)
            => CreateProcess(
                CreateProcessStartInfo(executable, arguments, workingDirectory, captureOutput, displayWindow, environmentVariables),
                lowPriority: lowPriority,
                onProcessStartHandler: onProcessStartHandler,
                onOutputDataReceived: onOutputDataReceived,
                cancellationToken: cancellationToken);
 
        public static ProcessInfo CreateProcess(
            ProcessStartInfo processStartInfo,
            bool lowPriority = false,
            Action<Process>? onProcessStartHandler = null,
            Action<DataReceivedEventArgs>? onOutputDataReceived = null,
            CancellationToken cancellationToken = default)
        {
            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)
                {
                    onOutputDataReceived?.Invoke(e);
                    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(async () =>
                {
                    int exitCode;
                    try
                    {
                        exitCode = await GetExitCodeAsync(process);
                    }
                    catch (Exception ex)
                    {
                        tcs.TrySetException(ex);
                        throw;
                    }
 
                    var result = new ProcessResult(
                        process,
                        exitCode,
                        new ReadOnlyCollection<string>(outputLines),
                        new ReadOnlyCollection<string>(errorLines));
                    tcs.TrySetResult(result);
 
                    static async ValueTask<int> GetExitCodeAsync(Process process)
                    {
                        await process.WaitForExitAsync();
                        return process.ExitCode;
                    }
                }, cancellationToken);
            };
 
            var registration = cancellationToken.Register(() =>
            {
                if (tcs.TrySetCanceled())
                {
                    // If the underlying process is still running, we should kill it
                    if (!process.HasExited)
                    {
                        try
                        {
                            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();
            }
 
            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;
        }
    }
}