File: Certificates\CertificateGeneration\CertificateProcessRunner.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
 
namespace Microsoft.AspNetCore.Certificates.Generation;
 
internal static class CertificateProcessRunner
{
    public static CertificateProcessResult Run(ProcessStartInfo startInfo, CancellationToken cancellationToken = default)
        => RunCore(startInfo, captureOutput: false, cancellationToken);
 
    public static CertificateProcessResult RunAndCaptureText(ProcessStartInfo startInfo, CancellationToken cancellationToken = default)
        => RunCore(startInfo, captureOutput: true, cancellationToken);
 
    private static CertificateProcessResult RunCore(ProcessStartInfo startInfo, bool captureOutput, CancellationToken cancellationToken)
    {
        using var process = Process.Start(startInfo) ?? throw new InvalidOperationException($"Failed to start process '{startInfo.FileName}'.");
 
        // Drain both redirected pipes concurrently. Waiting for the process before reading, or
        // reading one pipe to completion before the other, can deadlock when either pipe fills.
        var standardOutputTask = startInfo.RedirectStandardOutput
            ? ReadOutputAsync(process.StandardOutput, captureOutput, cancellationToken)
            : Task.FromResult(string.Empty);
        var standardErrorTask = startInfo.RedirectStandardError
            ? ReadOutputAsync(process.StandardError, captureOutput, cancellationToken)
            : Task.FromResult(string.Empty);
 
        try
        {
            Task.WhenAll(
                standardOutputTask,
                standardErrorTask,
                process.WaitForExitAsync(cancellationToken)).GetAwaiter().GetResult();
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            try
            {
                process.Kill(entireProcessTree: true);
                process.WaitForExit();
            }
            catch (Exception ex) when (ex is InvalidOperationException or NotSupportedException or System.ComponentModel.Win32Exception)
            {
                // The process either exited concurrently or could not be killed by this platform.
            }
 
            throw;
        }
 
        return new CertificateProcessResult(
            process.ExitCode,
            standardOutputTask.GetAwaiter().GetResult(),
            standardErrorTask.GetAwaiter().GetResult());
    }
 
    private static async Task<string> ReadOutputAsync(StreamReader reader, bool captureOutput, CancellationToken cancellationToken)
    {
        if (captureOutput)
        {
            return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
        }
 
        await reader.BaseStream.CopyToAsync(Stream.Null, cancellationToken).ConfigureAwait(false);
        return string.Empty;
    }
}
 
internal readonly record struct CertificateProcessResult(int ExitCode, string StandardOutput, string StandardError);