using System.Text; namespace VideoDownloader.Core.Platform; /// 프로세스 실행 명세. EnvPrefix 값은 기존 값 앞에 붙는다(PATH 선두 주입용). public sealed record ProcessSpec( string ExePath, IReadOnlyList Args, IReadOnlyDictionary? EnvPrefix = null); /// 프로세스 실행 결과. Stdout/Stderr 은 전체 누적(라인 탭과 별개). public sealed record ProcessResult(int ExitCode, string Stdout, string Stderr); /// /// 프로세스 실행 추상화(재구성 ② / M0 전제). 모바일(iOS)에선 구현체를 주입하지 않고 /// 데스크톱(Win/macOS/Linux)은 DesktopProcessRunner 사용. 단일 프리미티브로 /// yt-dlp 라인 스트리밍 · -J 캡처 · ffmpeg 리먹스/DASH 진행 파싱을 모두 커버한다. /// public interface IProcessRunner { /// 실행. onLine?(stream:"out"|"err", line) 실시간 탭. 취소 시 트리 킬. Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct); } /// 데스크톱 구현 — 기존 YtDlpRunner/HlsDownloader 의 검증된 안전 규칙: /// stdout/stderr 동시 드레인(데드락 방지), 취소/실패 시 entireProcessTree 킬, 인자 배열 전달. public sealed class DesktopProcessRunner : IProcessRunner { public static DesktopProcessRunner Default { get; } = new(); public async Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { System.Diagnostics.Process? proc = null; try { var psi = new System.Diagnostics.ProcessStartInfo(spec.ExePath) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, }; foreach (var a in spec.Args) psi.ArgumentList.Add(a); if (spec.EnvPrefix != null) { foreach (var (key, prefix) in spec.EnvPrefix) { var cur = psi.Environment.TryGetValue(key, out var existing) ? (existing ?? "") : ""; psi.Environment[key] = prefix + Path.PathSeparator + cur; } } proc = System.Diagnostics.Process.Start(psi) ?? throw new InvalidOperationException($"{spec.ExePath} 시작 실패"); await using var _ = ct.Register(() => { try { if (!proc.HasExited) proc.Kill(entireProcessTree: true); } catch { } }); var stdout = new System.Text.StringBuilder(); var stderr = new System.Text.StringBuilder(); var outTask = TapAsync(proc.StandardOutput, "out", stdout, onLine, ct); var errTask = TapAsync(proc.StandardError, "err", stderr, onLine, ct); await proc.WaitForExitAsync(ct); await Task.WhenAll(outTask, errTask); return new ProcessResult(proc.ExitCode, stdout.ToString(), stderr.ToString()); } finally { try { if (proc is { HasExited: false }) proc.Kill(entireProcessTree: true); } catch { } } } private static async Task TapAsync(StreamReader reader, string stream, StringBuilder acc, Action? onLine, CancellationToken ct) { string? line; while ((line = await reader.ReadLineAsync(ct)) != null) { acc.AppendLine(line); if (onLine != null && !string.IsNullOrWhiteSpace(line)) onLine(stream, line); } } }