using System.Diagnostics; namespace Sdt.Runner; public sealed record RunResult(int ExitCode, TimeSpan Elapsed) { public bool Success => ExitCode == 0; } public static class ProcessRunner { /// /// Runs a command with the given args, streaming stdout/stderr via . /// onOutput receives (line, isStderr). /// public static async Task RunAsync( string command, IEnumerable args, string workingDir, Action onOutput, CancellationToken cancellationToken = default) { var psi = new ProcessStartInfo { FileName = command, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = workingDir, }; foreach (var arg in args) psi.ArgumentList.Add(arg); var sw = Stopwatch.StartNew(); using var process = new Process { StartInfo = psi }; void OnCancel(object? sender, ConsoleCancelEventArgs e) { e.Cancel = true; // Prevent SDT from exiting immediately try { process.Kill(entireProcessTree: true); } catch { } } Console.CancelKeyPress += OnCancel; try { process.Start(); var stdoutTask = DrainAsync(process.StandardOutput, line => onOutput(line, false), cancellationToken); var stderrTask = DrainAsync(process.StandardError, line => onOutput(line, true), cancellationToken); await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); } finally { Console.CancelKeyPress -= OnCancel; } sw.Stop(); return new RunResult(process.ExitCode, sw.Elapsed); } private static async Task DrainAsync(StreamReader reader, Action emit, CancellationToken ct) { string? line; while ((line = await reader.ReadLineAsync(ct).ConfigureAwait(false)) is not null && !ct.IsCancellationRequested) { emit(line); } } }