refactor(2): Core Platform abstractions (IProcessRunner/IMediaMuxer + Desktop impls) - Hls/Dash/YtDlp through injection seams, threading contract documented (66 tests GREEN)
This commit is contained in:
parent
9eb988440f
commit
6df9f81b4a
6 changed files with 402 additions and 220 deletions
|
|
@ -70,11 +70,12 @@ public static class YtDlpRunner
|
|||
}
|
||||
|
||||
/// <summary>설치된 yt-dlp 버전 문자열("2026.03.17"). 알 수 없으면 null.</summary>
|
||||
public static async Task<string?> GetVersionAsync(string exe, CancellationToken ct)
|
||||
public static async Task<string?> GetVersionAsync(string exe, CancellationToken ct,
|
||||
Platform.IProcessRunner? runner = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (exit, stdout, _) = await RunCaptureAsync(exe, new[] { "--version" }, ct);
|
||||
var (exit, stdout, _) = await RunCaptureAsync(exe, new[] { "--version" }, ct, runner);
|
||||
if (exit != 0) return null;
|
||||
var first = stdout.Trim().Split('\n', StringSplitOptions.TrimEntries)[0];
|
||||
return string.IsNullOrWhiteSpace(first) ? null : first;
|
||||
|
|
@ -126,89 +127,24 @@ public static class YtDlpRunner
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>yt-dlp 자식 프로세스 PATH 앞에 ToolsDir 를 추가 → 번들 deno.exe(EJS n 챌린지 해석용) 를 yt-dlp 가 자동 발겑.
|
||||
/// 시스템 PATH 는 건드리지 않고 이 프로세스에만 적용. ffmpeg/yt-dlp 등 다른 번들 도구 발견에도 보조.</summary>
|
||||
private static void PrependToolsToPath(ProcessStartInfo psi)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cur = psi.Environment.TryGetValue("PATH", out var p) ? (p ?? "") : "";
|
||||
psi.Environment["PATH"] = ToolsDir + Path.PathSeparator + cur;
|
||||
}
|
||||
catch { } // UseShellExecute=true 등 예외 시 무시(본 프로젝트에선 항상 false)
|
||||
}
|
||||
|
||||
/// <summary>프로세스 실행. onLine(stream, line): stream="out"|"err". 취소 시 트리 킬.</summary>
|
||||
/// <summary>프로세스 실행. onLine(stream, line): stream="out"|"err". 취소 시 트리 킬.
|
||||
/// 재구성 ②: IProcessRunner 주입 가능(테스트/모바일) — 기본 DesktopProcessRunner.</summary>
|
||||
public static async Task<int> RunAsync(string exe, IEnumerable<string> args,
|
||||
Action<string, string> onLine, CancellationToken ct)
|
||||
Action<string, string> onLine, CancellationToken ct, Platform.IProcessRunner? runner = null)
|
||||
{
|
||||
Process? proc = null;
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo(exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
PrependToolsToPath(psi); // 번들 deno.exe 를 yt-dlp 가 발견하도록(EJS n 챌린지 해석)
|
||||
|
||||
proc = Process.Start(psi) ?? throw new InvalidOperationException("yt-dlp 시작 실패");
|
||||
await using var _ = ct.Register(() => { try { if (!proc.HasExited) proc.Kill(entireProcessTree: true); } catch { } });
|
||||
|
||||
// stdout/stderr 를 별개 태스크로 동시 드레인(파이프 버퍼 차면 데드락).
|
||||
var outTask = ReadLinesAsync(proc.StandardOutput, "out", onLine, ct);
|
||||
var errTask = ReadLinesAsync(proc.StandardError, "err", onLine, ct);
|
||||
await proc.WaitForExitAsync(ct);
|
||||
await Task.WhenAll(outTask, errTask);
|
||||
return proc.ExitCode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (proc is { HasExited: false }) proc.Kill(entireProcessTree: true); } catch { }
|
||||
}
|
||||
var r = runner ?? Platform.DesktopProcessRunner.Default;
|
||||
var res = await r.RunAsync(new Platform.ProcessSpec(exe, args.ToList(),
|
||||
EnvPrefix: new Dictionary<string, string> { ["PATH"] = ToolsDir }), onLine, ct);
|
||||
return res.ExitCode;
|
||||
}
|
||||
|
||||
/// <summary>stdout 전체를 하나의 문자열로 캡처(-J JSON 덤프용). stderr 도 버퍼링하며 취소 시 트리 킬.</summary>
|
||||
/// <summary>stdout 전체를 하나의 문자열로 캡처(-J JSON 덤프용). 재구성 ②: 러너 주입 가능.</summary>
|
||||
public static async Task<(int Exit, string Stdout, string Stderr)> RunCaptureAsync(
|
||||
string exe, IEnumerable<string> args, CancellationToken ct)
|
||||
string exe, IEnumerable<string> args, CancellationToken ct, Platform.IProcessRunner? runner = null)
|
||||
{
|
||||
Process? proc = null;
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo(exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
PrependToolsToPath(psi); // 번들 deno.exe 를 yt-dlp 가 발견하도록(EJS n 챌린지 해석)
|
||||
|
||||
proc = Process.Start(psi) ?? throw new InvalidOperationException("yt-dlp 시작 실패");
|
||||
await using var _ = ct.Register(() => { try { if (!proc.HasExited) proc.Kill(entireProcessTree: true); } catch { } });
|
||||
|
||||
var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct);
|
||||
var stderrTask = proc.StandardError.ReadToEndAsync(ct);
|
||||
await proc.WaitForExitAsync(ct);
|
||||
return (proc.ExitCode, await stdoutTask, await stderrTask);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (proc is { HasExited: false }) proc.Kill(entireProcessTree: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReadLinesAsync(StreamReader reader, string stream, Action<string, string> onLine, CancellationToken ct)
|
||||
{
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(ct)) != null)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (!string.IsNullOrWhiteSpace(line)) onLine(stream, line);
|
||||
}
|
||||
var r = runner ?? Platform.DesktopProcessRunner.Default;
|
||||
var res = await r.RunAsync(new Platform.ProcessSpec(exe, args.ToList(),
|
||||
EnvPrefix: new Dictionary<string, string> { ["PATH"] = ToolsDir }), null, ct);
|
||||
return (res.ExitCode, res.Stdout, res.Stderr);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue