| File: Dcp\Process\ProcessUtil.Capture.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// 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; using System.Text; namespace Aspire.Hosting.Dcp.Process; internal static partial class ProcessUtil { // Captured text must preserve NUL delimiters and newlines, while legacy line execution // retains its callback and WriteLine behavior. These paths are separate temporarily; // https://github.com/microsoft/aspire/issues/21540 tracks consolidating lifetime/output handling. public static async Task<ProcessCaptureResult> RunAndCaptureTextAsync( string executablePath, IReadOnlyList<string> arguments, string workingDirectory, IReadOnlyDictionary<string, string>? environmentVariables = null, ReadOnlyMemory<byte> standardInput = default, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var process = new System.Diagnostics.Process { StartInfo = new ProcessStartInfo(executablePath) { WorkingDirectory = workingDirectory, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, UseShellExecute = false, CreateNoWindow = true } }; foreach (var argument in arguments) { process.StartInfo.ArgumentList.Add(argument); } if (environmentVariables is not null) { foreach (var (key, value) in environmentVariables) { process.StartInfo.Environment[key] = value; } } process.Start(); using var batch = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var output = process.StandardOutput.ReadToEndAsync(batch.Token); var error = process.StandardError.ReadToEndAsync(batch.Token); // Concurrent WaitForExitAsync calls can lose Process.Exited handlers; share one wait. var exit = process.WaitForExitAsync(CancellationToken.None); IOException? inputError = null; try { // Start all three streams before waiting: a child can fill either output pipe // before reading input, and input itself can exceed pipe capacity. await Task.WhenAll(GuardAsync(output), GuardAsync(error), GuardAsync(WriteAsync()), GuardAsync(exit.WaitAsync(batch.Token))).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (inputError is not null && process.ExitCode == 0) { throw inputError; } return new(process.ExitCode, await output.ConfigureAwait(false), await error.ConfigureAwait(false)); } catch { await StopCapturedProcessAsync(process, exit).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); throw; } async Task WriteAsync() { try { await process.StandardInput.BaseStream.WriteAsync(standardInput, batch.Token).ConfigureAwait(false); } catch (IOException exception) { // A child can reject input and close its pipe before emitting its final diagnostic. // Drain output and wait for its exit; a nonzero result carries that diagnostic. inputError = exception; } finally { // Only the underlying stream is used; closing the writer can flush a broken pipe. process.StandardInput.BaseStream.Close(); } } async Task GuardAsync(Task task) { try { await task.ConfigureAwait(false); } catch { batch.Cancel(); await StopCapturedProcessAsync(process, exit).ConfigureAwait(false); throw; } } } private static async Task StopCapturedProcessAsync(System.Diagnostics.Process process, Task exit) { try { if (!process.HasExited) { process.Kill(entireProcessTree: true); } } catch (Exception exception) when ((exception is InvalidOperationException or System.ComponentModel.Win32Exception) && process.HasExited) { // Exit can race the kill. Still wait before relinquishing process ownership. await exit.ConfigureAwait(false); return; } await exit.ConfigureAwait(false); } }