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);
}
}
}