using System.Diagnostics; using System.Globalization; using System.Text; using System.Text.RegularExpressions; using System.IO; namespace AmigaDB.VideoRenderer.Services; public sealed partial class ProcessRunner { public async Task RunAsync( string executable, IEnumerable arguments, Action? log, Action? position, CancellationToken token, bool allowFailure = false) { ProcessStartInfo start = new() { FileName = executable, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, StandardErrorEncoding = Encoding.UTF8, StandardOutputEncoding = Encoding.UTF8 }; foreach (string argument in arguments) start.ArgumentList.Add(argument); using Process process = new() { StartInfo = start, EnableRaisingEvents = true }; StringBuilder output = new(); process.Start(); Task stdout = PumpAsync(process.StandardOutput, output, log, position, token); Task stderr = PumpAsync(process.StandardError, output, log, position, token); using CancellationTokenRegistration registration = token.Register(() => { try { if (!process.HasExited) process.Kill(true); } catch { } }); await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(token)); if (process.ExitCode != 0 && !allowFailure) throw new InvalidOperationException($"Process exited with code {process.ExitCode}.\n{output}"); return output.ToString(); } private static async Task PumpAsync( StreamReader reader, StringBuilder output, Action? log, Action? position, CancellationToken token) { while (await reader.ReadLineAsync(token) is { } line) { output.AppendLine(line); log?.Invoke(line); Match match = TimeRegex().Match(line); if (match.Success && TimeSpan.TryParse(match.Groups[1].Value, CultureInfo.InvariantCulture, out TimeSpan time)) position?.Invoke(time); } } [GeneratedRegex(@"time=(\d{2}:\d{2}:\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)] private static partial Regex TimeRegex(); }