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:
+23
-45
@@ -1,7 +1,7 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { existsSync, mkdirSync, copyFileSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import { getYtdlpPath, getBundledYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
|
||||
import { execFileAsync } from './lib/exec'
|
||||
import { VERSION_TIMEOUT_MS, YTDLP_UPDATE_TIMEOUT_MS } from './constants'
|
||||
import { getSettings, setSettings } from './settings'
|
||||
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
||||
@@ -34,33 +34,21 @@ export function ensureManagedYtdlp(): void {
|
||||
}
|
||||
|
||||
/** Spawn the bundled yt-dlp and read back its `--version` for the Settings panel. */
|
||||
export function getYtdlpVersion(): Promise<YtdlpVersionResult> {
|
||||
export async function getYtdlpVersion(): Promise<YtdlpVersionResult> {
|
||||
const ytdlpPath = getYtdlpPath()
|
||||
|
||||
if (!existsSync(ytdlpPath)) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
error: YTDLP_MISSING_MSG
|
||||
})
|
||||
return { ok: false, error: YTDLP_MISSING_MSG }
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
execFile(
|
||||
ytdlpPath,
|
||||
['--version'],
|
||||
{ windowsHide: true, timeout: VERSION_TIMEOUT_MS },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const msg = (err as { killed?: boolean }).killed
|
||||
? 'Timed out running yt-dlp.'
|
||||
: (stderr || err.message).trim()
|
||||
resolve({ ok: false, error: msg })
|
||||
return
|
||||
}
|
||||
resolve({ ok: true, version: stdout.trim() })
|
||||
}
|
||||
)
|
||||
})
|
||||
const r = await execFileAsync(ytdlpPath, ['--version'], { timeout: VERSION_TIMEOUT_MS })
|
||||
if (!r.ok) {
|
||||
const msg = r.timedOut
|
||||
? 'Timed out running yt-dlp.'
|
||||
: (r.stderr || r.error?.message || '').trim()
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
return { ok: true, version: r.stdout.trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,13 +57,13 @@ export function getYtdlpVersion(): Promise<YtdlpVersionResult> {
|
||||
* yt-dlp.exe lives in, which holds for both the dev resources/bin checkout and
|
||||
* a per-user install; it would fail under a locked-down system install.
|
||||
*/
|
||||
export function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateResult> {
|
||||
export async function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateResult> {
|
||||
// Validate against the channel allowlist BEFORE the value reaches `--update-to`.
|
||||
// That flag also accepts `OWNER/REPO@TAG`, which would download and install an
|
||||
// arbitrary binary over yt-dlp.exe — so an unrecognised value (e.g. forged by a
|
||||
// compromised renderer over IPC) must never be forwarded. (audit F1)
|
||||
if (!isYtdlpUpdateChannel(channel)) {
|
||||
return Promise.resolve({ ok: false, error: 'Unsupported update channel.' })
|
||||
return { ok: false, error: 'Unsupported update channel.' }
|
||||
}
|
||||
|
||||
// Self-heal: restore the managed copy from the bundled seed before updating, so
|
||||
@@ -84,29 +72,19 @@ export function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateRes
|
||||
const ytdlpPath = getYtdlpPath()
|
||||
|
||||
if (!existsSync(ytdlpPath)) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
error: YTDLP_MISSING_MSG
|
||||
})
|
||||
return { ok: false, error: YTDLP_MISSING_MSG }
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
execFile(
|
||||
ytdlpPath,
|
||||
['--update-to', channel],
|
||||
{ windowsHide: true, timeout: YTDLP_UPDATE_TIMEOUT_MS },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
const msg = (err as { killed?: boolean }).killed
|
||||
? 'Timed out updating yt-dlp.'
|
||||
: (stderr || err.message).trim()
|
||||
resolve({ ok: false, error: msg })
|
||||
return
|
||||
}
|
||||
resolve({ ok: true, output: stdout.trim() })
|
||||
}
|
||||
)
|
||||
const r = await execFileAsync(ytdlpPath, ['--update-to', channel], {
|
||||
timeout: YTDLP_UPDATE_TIMEOUT_MS
|
||||
})
|
||||
if (!r.ok) {
|
||||
const msg = r.timedOut
|
||||
? 'Timed out updating yt-dlp.'
|
||||
: (r.stderr || r.error?.message || '').trim()
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
return { ok: true, output: r.stdout.trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user