using System.Diagnostics;
using System.Text;
namespace Everything2Everything.Core.Converters;
public sealed record ProcessRunResult(int ExitCode, string StdOut, string StdErr, bool TimedOut)
{
public bool Success => !TimedOut && ExitCode == 0;
}
///
/// 외부 CLI 프로세스 실행을 일원화한다 — stdout/stderr 수집, 타임아웃, 취소 시 프로세스 트리 종료.
/// LibreOffice·FFmpeg·Ghostscript·qpdf 등 모든 외부 도구 어댑터가 공유하는 실행기.
///
public static class ExternalProcessRunner
{
public static async Task RunAsync(
string fileName,
IEnumerable arguments,
TimeSpan? timeout = null,
string? workingDirectory = null,
CancellationToken cancellationToken = default)
{
var psi = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (var a in arguments) psi.ArgumentList.Add(a);
if (!string.IsNullOrEmpty(workingDirectory)) psi.WorkingDirectory = workingDirectory;
using var proc = new Process { StartInfo = psi };
var stdout = new StringBuilder();
var stderr = new StringBuilder();
proc.OutputDataReceived += (_, e) => { if (e.Data is not null) stdout.AppendLine(e.Data); };
proc.ErrorDataReceived += (_, e) => { if (e.Data is not null) stderr.AppendLine(e.Data); };
if (!proc.Start())
throw new InvalidOperationException($"프로세스를 시작하지 못했습니다: {fileName}");
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
if (timeout is { } t) linked.CancelAfter(t);
var timedOut = false;
try
{
await proc.WaitForExitAsync(linked.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 사용자 취소면 전파, 타임아웃이면 TimedOut으로 보고
timedOut = !cancellationToken.IsCancellationRequested;
try { proc.Kill(entireProcessTree: true); } catch { /* 이미 종료 */ }
if (cancellationToken.IsCancellationRequested)
{
try { proc.WaitForExit(2000); } catch { }
throw;
}
}
// Kill 또는 종료 후 비동기 출력 콜백이 마저 도착하도록 잠깐 대기
try { proc.WaitForExit(2000); } catch { }
var exitCode = timedOut ? -1 : SafeExitCode(proc);
return new ProcessRunResult(exitCode, stdout.ToString(), stderr.ToString(), timedOut);
}
private static int SafeExitCode(Process proc)
{
try { return proc.ExitCode; }
catch { return -1; }
}
}