refactor(main): execFileAsync helper — unify the 5 spawn-and-read sites
Replace the hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers in
probe / ytdlp (×2) / ffmpeg / indexer.probeFlat / download.probeMeta with one
lib/exec.ts execFileAsync returning a normalized
{ok,stdout,stderr,timedOut,error} that never rejects. Each caller keeps its own
error mapping (timedOut -> "Timed out …" vs stderr) and JSON parsing; behaviour
is unchanged. windowsHide defaults on; per-call timeout/maxBuffer preserved.
Foundation for CC4/CC5 — the stdout line-buffer half lands with SIMP5, the
net.request half with SIMP16, at which point those checkboxes close.
Unit-tested (exec.test.ts) against the node binary: ok / stderr+nonzero-exit /
missing-binary / timeout. typecheck + 262 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { execFile, type ExecFileOptions } from 'child_process'
|
||||
|
||||
/**
|
||||
* Normalized result of a one-shot child-process run. `ok` is true on a clean
|
||||
* exit-0; `timedOut` distinguishes a kill-by-timeout (Node sets `err.killed` when
|
||||
* it terminates the process on the `timeout` option) from an ordinary non-zero
|
||||
* exit, so callers can show "Timed out …" versus the captured stderr.
|
||||
*/
|
||||
export interface ExecResult {
|
||||
ok: boolean
|
||||
stdout: string
|
||||
stderr: string
|
||||
timedOut: boolean
|
||||
/** The raw error on a non-zero exit / spawn failure (undefined when ok). */
|
||||
error?: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* `execFile` as a promise that never rejects (audit SIMP2/CC4/CC5). Replaces the
|
||||
* six hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers across
|
||||
* probe / ytdlp (×2) / ffmpeg / indexer / download.probeMeta / schedule — each
|
||||
* caller now maps this normalized result to its own shape instead of repeating the
|
||||
* `err.killed` timeout check and the resolve/reject plumbing.
|
||||
*
|
||||
* `windowsHide` defaults on (every caller wants the console window suppressed);
|
||||
* pass `timeout` / `maxBuffer` per call exactly as before.
|
||||
*/
|
||||
export function execFileAsync(
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: ExecFileOptions = {}
|
||||
): Promise<ExecResult> {
|
||||
return new Promise((resolve) => {
|
||||
execFile(file, [...args], { windowsHide: true, ...options }, (err, stdout, stderr) => {
|
||||
// With the default (utf8) encoding stdout/stderr are strings, but ExecFileOptions
|
||||
// permits a Buffer encoding, so normalize defensively.
|
||||
const out = typeof stdout === 'string' ? stdout : stdout.toString()
|
||||
const errOut = typeof stderr === 'string' ? stderr : stderr.toString()
|
||||
if (err) {
|
||||
resolve({
|
||||
ok: false,
|
||||
stdout: out,
|
||||
stderr: errOut,
|
||||
timedOut: (err as { killed?: boolean }).killed === true,
|
||||
error: err
|
||||
})
|
||||
} else {
|
||||
resolve({ ok: true, stdout: out, stderr: errOut, timedOut: false })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user