using System.Diagnostics; using System.Text; namespace MeetingAssistant.Transcription; public interface ICommandRunner { Task RunAsync( string fileName, IReadOnlyList arguments, CancellationToken cancellationToken, IReadOnlyDictionary? environment = null); } public sealed class ProcessCommandRunner : ICommandRunner { private readonly ILogger logger; public ProcessCommandRunner(ILogger logger) { this.logger = logger; } public async Task RunAsync( string fileName, IReadOnlyList arguments, CancellationToken cancellationToken, IReadOnlyDictionary? environment = null) { var startInfo = CreateStartInfo(fileName, arguments, environment); logger.LogInformation("Running command {Command} {Arguments}", fileName, string.Join(' ', arguments)); using var process = Process.Start(startInfo) ?? throw new InvalidOperationException($"Failed to start command '{fileName}'."); var outputTask = ReadLinesAsync(process.StandardOutput, line => logger.LogInformation("{Command}: {Line}", fileName, line), cancellationToken); var errorTask = ReadLinesAsync(process.StandardError, line => logger.LogWarning("{Command}: {Line}", fileName, line), cancellationToken); try { await process.WaitForExitAsync(cancellationToken); } catch (OperationCanceledException) { if (!process.HasExited) { logger.LogWarning("Cancelling command {Command}; killing process tree.", fileName); process.Kill(entireProcessTree: true); } throw; } var result = new CommandResult(process.ExitCode, await outputTask, await errorTask); if (result.ExitCode != 0) { logger.LogWarning( "Command {Command} exited with {ExitCode}: {Error}", fileName, result.ExitCode, result.StandardError); } return result; } internal static ProcessStartInfo CreateStartInfo( string fileName, IReadOnlyList arguments, IReadOnlyDictionary? environment = null) { var startInfo = new ProcessStartInfo { FileName = fileName, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden }; foreach (var argument in arguments) { startInfo.ArgumentList.Add(argument); } if (environment is not null) { foreach (var (key, value) in environment) { startInfo.Environment[key] = value; } } return startInfo; } private static async Task ReadLinesAsync( StreamReader reader, Action log, CancellationToken cancellationToken) { var output = new StringBuilder(); while (await reader.ReadLineAsync(cancellationToken) is { } line) { output.AppendLine(line); log(line); } return output.ToString(); } } public sealed record CommandResult(int ExitCode, string StandardOutput, string StandardError);