journal/Journal.DevTool/Runner/ProcessRunner.cs

61 lines
1.8 KiB
C#

using System.Diagnostics;
namespace Sdt.Runner;
public sealed record RunResult(int ExitCode, TimeSpan Elapsed)
{
public bool Success => ExitCode == 0;
}
public static class ProcessRunner
{
/// <summary>
/// Runs a command with the given args, streaming stdout/stderr via <paramref name="onOutput"/>.
/// onOutput receives (line, isStderr).
/// </summary>
public static async Task<RunResult> RunAsync(
string command,
IEnumerable<string> args,
string workingDir,
Action<string, bool> 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 };
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);
sw.Stop();
return new RunResult(process.ExitCode, sw.Elapsed);
}
private static async Task DrainAsync(StreamReader reader, Action<string> emit, CancellationToken ct)
{
string? line;
while ((line = await reader.ReadLineAsync(ct).ConfigureAwait(false)) is not null
&& !ct.IsCancellationRequested)
{
emit(line);
}
}
}