83 lines
3.6 KiB
C#
83 lines
3.6 KiB
C#
using System.Text;
|
|
namespace VideoDownloader.Core.Platform;
|
|
|
|
/// <summary>프로세스 실행 명세. EnvPrefix 값은 기존 값 앞에 붙는다(PATH 선두 주입용).</summary>
|
|
public sealed record ProcessSpec(
|
|
string ExePath,
|
|
IReadOnlyList<string> Args,
|
|
IReadOnlyDictionary<string, string>? EnvPrefix = null);
|
|
|
|
/// <summary>프로세스 실행 결과. Stdout/Stderr 은 전체 누적(라인 탭과 별개).</summary>
|
|
public sealed record ProcessResult(int ExitCode, string Stdout, string Stderr);
|
|
|
|
/// <summary>
|
|
/// 프로세스 실행 추상화(재구성 ② / M0 전제). 모바일(iOS)에선 구현체를 주입하지 않고
|
|
/// 데스크톱(Win/macOS/Linux)은 DesktopProcessRunner 사용. 단일 프리미티브로
|
|
/// yt-dlp 라인 스트리밍 · -J 캡처 · ffmpeg 리먹스/DASH 진행 파싱을 모두 커버한다.
|
|
/// </summary>
|
|
public interface IProcessRunner
|
|
{
|
|
/// <summary>실행. onLine?(stream:"out"|"err", line) 실시간 탭. 취소 시 트리 킬.</summary>
|
|
Task<ProcessResult> RunAsync(ProcessSpec spec, Action<string, string>? onLine, CancellationToken ct);
|
|
}
|
|
|
|
/// <summary>데스크톱 구현 — 기존 YtDlpRunner/HlsDownloader 의 검증된 안전 규칙:
|
|
/// stdout/stderr 동시 드레인(데드락 방지), 취소/실패 시 entireProcessTree 킬, 인자 배열 전달.</summary>
|
|
public sealed class DesktopProcessRunner : IProcessRunner
|
|
{
|
|
public static DesktopProcessRunner Default { get; } = new();
|
|
|
|
public async Task<ProcessResult> RunAsync(ProcessSpec spec, Action<string, string>? 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<string, string>? onLine, CancellationToken ct)
|
|
{
|
|
string? line;
|
|
while ((line = await reader.ReadLineAsync(ct)) != null)
|
|
{
|
|
acc.AppendLine(line);
|
|
if (onLine != null && !string.IsNullOrWhiteSpace(line)) onLine(stream, line);
|
|
}
|
|
}
|
|
}
|