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,
IReadOnlyDictionary? envOverrides = null,
CancellationToken cancellationToken = default)
{
var psi = new ProcessStartInfo
{
FileName = Core.CommandResolver.Resolve(command),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDir,
};
if (envOverrides is not null)
{
foreach (var kvp in envOverrides)
psi.Environment[kvp.Key] = kvp.Value;
}
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);
}
}
}