File: ProcessEx.cs
Web Access
Project: src\src\Tools\Microsoft.dotnet-openapi\test\dotnet-microsoft.openapi.Tests.csproj (Microsoft.DotNet.Open.Api.Tools.Tests)
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Xunit.Abstractions;
 
namespace Microsoft.Extensions.Internal;
 
internal class ProcessEx : IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly Process _process;
    private readonly StringBuilder _stderrCapture = new StringBuilder();
    private readonly StringBuilder _stdoutCapture = new StringBuilder();
    private readonly object _pipeCaptureLock = new object();
    private BlockingCollection<string> _stdoutLines = new BlockingCollection<string>();
    private TaskCompletionSource<int> _exited;
 
    private ProcessEx(ITestOutputHelper output, Process proc)
    {
        _output = output;
 
        _process = proc;
        proc.EnableRaisingEvents = true;
        proc.OutputDataReceived += OnOutputData;
        proc.ErrorDataReceived += OnErrorData;
        proc.Exited += OnProcessExited;
        proc.BeginOutputReadLine();
        proc.BeginErrorReadLine();
 
        _exited = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
    }
 
    public Task Exited => _exited.Task;
 
    public bool HasExited => _process.HasExited;
 
    public string Output
    {
        get
        {
            lock (_pipeCaptureLock)
            {
                return _stdoutCapture.ToString();
            }
        }
    }
 
    public int ExitCode => _process.ExitCode;
 
    public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary<string, string> envVars = null)
    {
        var startInfo = new ProcessStartInfo(command, args)
        {
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WorkingDirectory = workingDirectory
        };
 
        if (envVars != null)
        {
            foreach (var envVar in envVars)
            {
                startInfo.EnvironmentVariables[envVar.Key] = envVar.Value;
            }
        }
 
        output.WriteLine($"==> {startInfo.FileName} {startInfo.Arguments} [{startInfo.WorkingDirectory}]");
        var proc = Process.Start(startInfo);
 
        return new ProcessEx(output, proc);
    }
 
    private void OnErrorData(object sender, DataReceivedEventArgs e)
    {
        if (e.Data == null)
        {
            return;
        }
 
        lock (_pipeCaptureLock)
        {
            _stderrCapture.AppendLine(e.Data);
        }
 
        _output.WriteLine("[ERROR] " + e.Data);
    }
 
    private void OnOutputData(object sender, DataReceivedEventArgs e)
    {
        if (e.Data == null)
        {
            return;
        }
 
        lock (_pipeCaptureLock)
        {
            _stdoutCapture.AppendLine(e.Data);
        }
 
        _output.WriteLine(e.Data);
 
        if (_stdoutLines != null)
        {
            _stdoutLines.Add(e.Data);
        }
    }
 
    private void OnProcessExited(object sender, EventArgs e)
    {
        _process.WaitForExit();
        _stdoutLines.CompleteAdding();
        _stdoutLines = null;
        _exited.TrySetResult(_process.ExitCode);
    }
 
    public void Dispose()
    {
        if (_process != null && !_process.HasExited)
        {
            _process.KillTree();
        }
 
        _process.CancelOutputRead();
        _process.CancelErrorRead();
 
        _process.ErrorDataReceived -= OnErrorData;
        _process.OutputDataReceived -= OnOutputData;
        _process.Exited -= OnProcessExited;
        _process.Dispose();
 
        if (_stdoutLines != null)
        {
            _stdoutLines.Dispose();
        }
    }
}