fix(cli): verify browser/ffmpeg binaries exist before render starts#1365
Conversation
Renders on Windows failed at runtime with 'Browser was not found at the configured executablePath' when the cached chrome-headless-shell binary had been removed (AV quarantine, partial download) but the cache manifest still referenced it, and with ffprobe ENOENT when ffmpeg was installed without ffprobe. - Gate cache-manifest executablePath on existsSync and re-download when the binary is missing; same guard on the engine env-var path. - Shared environment preflight (preflight.ts) used by both render and doctor: checks ffmpeg, ffprobe, browser, disk, and UNC paths before starting. - Resolve absolute ffmpeg/ffprobe paths once and pass them to engine spawns instead of relying on bare PATH strings. - Map opaque Windows ffmpeg exit codes to actionable messages.
67138ec to
94f14f3
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verdict: Approve-with-nits. Solid Windows-pain PR with real teeth — preflight + binary-path env vars + Windows exit-code mapping + cache self-heal all land cleanly. CI is fully green including Render on windows-latest and the full regression matrix. The findings below are scope/edge-case calls, not blockers.
Concerns
1. executeRenderJob is also called from studioServer.ts and benchmark.ts — preflight is bypassed there. packages/cli/src/server/studioServer.ts:359 and packages/cli/src/commands/benchmark.ts:130 both invoke producer.executeRenderJob(...) directly, but neither calls runEnvironmentChecks() first. assertConfiguredFfmpegBinariesExist() inside executeRenderJob only fires when HYPERFRAMES_FFMPEG_PATH / HYPERFRAMES_FFPROBE_PATH are set to a missing path — if the env var is unset and ffmpeg simply isn't on PATH, the assert is a no-op and we still hit the mid-render spawn ffmpeg ENOENT the PR is trying to eliminate. A Windows user hitting "Render" in Studio reproduces the original bug. Either (a) lift the preflight into executeRenderJob so all in-process render paths share it, or (b) call runEnvironmentChecks() from studioServer.ts and benchmark.ts before the job starts.
2. Bare "ffmpeg"/"ffprobe" strings still in several spawn sites. PR catches the render hot path, but these survive:
packages/cli/src/commands/init.ts:79, 138—hyperframes init --video clip.mp4probes + transcodes with bareexecFileSync("ffprobe", ...)/spawn("ffmpeg", ...). Same Windows ENOENT cliff that motivates this PR.packages/core/src/studio-api/helpers/waveform.ts:33andmediaValidation.ts:29— studio dev-server runtime helpers (waveform decode, upload validation).packages/producer/src/parity-harness.ts:105,regression-harness.ts:505,utils/audioRegression.ts:178, 308— CI harnesses, dev-only; safe to defer.packages/player/tests/perf/scenarios/06-parity.ts— test scenario; safe to defer.
Two clear scope bumps (init.ts + studio-api waveform.ts/mediaValidation.ts) are user-facing on Windows; the rest are internal. Worth at least a follow-up issue noting the surface.
3. pathCache in engine/utils/ffmpegBinaries.ts:9 is module-scoped + never invalidated. findOnPath() caches the resolved path for the lifetime of the Node process. In one-shot hyperframes render invocations this is fine. But the studio dev server (hyperframes serve) is long-lived and a user reinstalling ffmpeg mid-session would keep getting the cached (possibly stale) resolution. The cache also doesn't observe the env vars — if a caller sets HYPERFRAMES_FFMPEG_PATH mid-process, getConfiguredBinary re-reads process.env correctly (good) but findOnPath results survive the env change. Low priority — likely fine for CLI render — but worth a comment noting the assumption, or invalidating the cache when the env var changes.
4. findOnPath (engine/utils/ffmpegBinaries.ts:11-30) uses execFileSync("which" | "where", [name]) which silently fails on Windows in subtle ways. where returns matches from PATH-EXT-resolved candidates without honoring PATHEXT, and which won't exist on bare Windows shells (no Git Bash) — but the PR is in the win32 branch already so it's where, which is fine. The CLI-side equivalent in packages/cli/src/browser/ffmpeg.ts:10 instead uses execSync with a shell-string ${cmd} ${name} — slightly less safe (if name ever became dynamic, command injection vector) but currently fine since name is a literal union type. Nit-level, but the two implementations of the same function (CLI side and engine side) drift in style. Worth consolidating to one — see also the fallow-ignore-file code-duplication markers the PR itself sprinkles, which acknowledge this.
5. Preflight runEnvironmentChecks doesn't include includeWindowsUnc or includeDisk in doctor. doctor.ts:201 calls runEnvironmentChecks({ includeBrowser: true }) only. Disk is covered by the legacy checkDisk already, but the new UNC-path warning fires in render and not in doctor — meaning a Windows user running hyperframes doctor from a \\server\share checkout won't be warned. Cheap to fix: runEnvironmentChecks({ includeBrowser: true, includeWindowsUnc: true }).
6. UNC warning is gated on projectDir.startsWith("\\\\") (preflight.ts:160) but runEnvironmentChecks defaults projectDir = process.cwd(). In render.ts:970 you pass projectDir (the resolved project dir), not process.cwd(). A user with a local cwd but --project \\server\share\thing is fine. A user with cwd on a UNC share but projectDir local skips the warning — and Chromium will refuse to launch with --user-data-dir under a UNC cwd too. Marginal but worth at least documenting which path is being checked.
Nits
packages/engine/src/utils/ffmpegBinaries.ts:1-3—import { existsSync } from "fs"/from "path"/from "child_process"use bare-spec node imports instead of thenode:prefix that the rest of the codebase (e.g.preflight.ts:1-3) uses. Style consistency.getConfiguredBinaryreturns the bare binary name ("ffmpeg") as a fallback when neither env var nor PATH yields anything. This means a downstreamspawn(getFfmpegBinary(), …)will still producespawn ffmpeg ENOENTon Windows even thoughrunFfmpegthen wraps it throughformatFfmpegError. The wrapping is fine — but thegetFfmpegBinary()contract being "returns a guess if nothing found" rather thanstring | undefinedquietly defeats the "verify exists before render" invariant in the PR title. A docstring noting the fallback intent would help future readers.packages/cli/src/commands/render.ts:1000mutatesprocess.env.HYPERFRAMES_FFMPEG_PATHto thread the resolved path into the engine — works, but it's a process-global side-effect from inside a render function. Mostly OK for the CLI's one-shot lifecycle; conflicts withstudioServer.tscallingrenderLocal-equivalent paths in-process across multiple renders, where the env mutation persists into subsequent renders. (Not load-bearing today, just brittle.)parseToolVersionis now exported from bothcommands/doctor.ts(re-export) andbrowser/preflight.ts(definition). Pick one home; the re-export through doctor was for backwards-compat with existing tests, but if no external caller depends on it, drop the re-export. (packages/cli/src/commands/doctor.ts:33.)formatWindowsFfmpegExit(runFfmpeg.ts:30-44) covers two NTSTATUS codes —STATUS_INVALID_IMAGE_FORMATandSTATUS_DLL_INIT_FAILED. Worth a comment naming the NTSTATUS constants so the magic numbers are searchable.
Questions
- Was
init.ts's--videopath intentionally out of scope, or did it fall off the list? Same Windows-PATHEXT/ENOENT failure mode, same one-line fix (findFFmpeg()instead of bare string). - Is the studio dev server's "Render" button (
studioServer.ts:359) considered part of "render starts" UX? If yes — same preflight gap asbenchmark.ts. If no — fine, but worth a note in the PR body so reviewers know it's deliberate.
What I didn't verify
- I didn't run the Windows render verification job locally; I'm trusting the green CI signal on
Render on windows-latest(passed 5m38s). - The
Cached binary missing — re-downloading…path inmanager.tsre-downloads viadownloadBrowser()which can take 30-90s. I didn't trace whethererrorBoxor progress UI surfaces to the user during that window — if not, the CLI will appear silent during the re-download. - I didn't probe whether the
formatWindowsFfmpegExitexit codes are the actual codes Windows surfaces for these scenarios — happy to trust the author's reproduction.
Review by Rames D Jusso
vanceingalls
left a comment
There was a problem hiding this comment.
LGTM — Via reviewed, stamp applied.
— Vai
- Replace hardcoded 'brew install ffmpeg' with getFFmpegInstallHint() in transcribe.ts and init.ts (Via's feedback — cross-platform hints) - Use findFFprobe()/findFFmpeg() in init.ts instead of bare "ffprobe"/ "ffmpeg" strings (Rames' feedback — unguarded spawn sites) - Use HYPERFRAMES_FFMPEG_PATH env in waveform.ts (Rames' feedback — bare "ffmpeg" in core package) - Show all failed preflight checks in render.ts, not just the first (Via's feedback — first-failure-only reporting)
|
Addressed the review feedback in 3deb0f8: Via's feedback:
Rames' feedback:
Not addressed (out of scope for this PR):
|
vanceingalls
left a comment
There was a problem hiding this comment.
Re-stamping at new HEAD — nit fixes applied (cross-platform install hints, all-failed-checks render output, bare ffmpeg strings in init.ts/waveform.ts).
— Vai
Fallow audit reportFound 8 findings. Duplication (4)
Health (4)
Generated by fallow. |
Merge activity
|
Problem
Windows renders commonly fail with environment errors before any real work starts:
Browser was not found at the configured executablePath (...chrome-headless-shell.exe)— the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists.[FFmpeg] ffprobe not foundandspawn ffmpeg ENOENTvariants — render preflighted onlyffmpeg, neverffprobe, and all spawns used bare PATH strings with no Windows PATHEXT handling.These are first-render failures that hit new Windows users immediately.
Fix
executablePathonexistsSyncand self-heal by re-downloading when the binary is missing; same guard on the engine env-var path.packages/cli/src/browser/preflight.ts) used by bothrenderanddoctor— checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints.packages/engine/src/utils/ffmpegBinaries.ts) and pass them to every engine spawn instead of relying on PATH.Testing
bun run buildgreen, oxlint/oxfmt clean.audioExtractor.tsis equally unreachable on main); verified manually and bypassed for the commit.