Skip to content

fix(producer): fall back to copying extracted frames when symlink hits EPERM [P1] - #1959

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/frames-symlink-eperm-copy-fallback
Jul 7, 2026
Merged

fix(producer): fall back to copying extracted frames when symlink hits EPERM [P1]#1959
miguel-heygen merged 3 commits into
mainfrom
fix/frames-symlink-eperm-copy-fallback

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Root cause

materializeExtractedFramesForCompiledDir stages each video's extracted frames into the compiled dir via a single symlink (the in-process renderer's default; distributed plan() already copies via materializeSymlinks). Windows without Developer Mode (or Administrator) cannot create symlinks and rejects with EPERM, so high/standard-quality renders failed there — while draft quality worked because it avoids the symlinked-cache path entirely (the reporter's exact observation).

Developer Mode is off by default on Windows, so this hits a common configuration.

Fix

A new stageExtractedFrameDir helper catches EPERM/EACCES from symlinkSync and falls back to the same recursive cpSync the materializeSymlinks path already uses. The extra disk is far better than a hard render failure. Non-permission errors (ENOSPC, etc.) still propagate so real failures aren't masked as silent copies. (Extracting the helper also keeps the caller under fallow's complexity gate.)

Test plan

Two new cases via the injected fileSystem:

  • symlinkSync throwing EPERM triggers exactly one recursive cpSync, with frames still remapped under compiledDir;
  • a non-permission error (ENOSPC) rethrows without falling back to copy.

Full renderOrchestrator.test.ts suite (81 tests) passes; producer build + typecheck clean; oxlint/oxfmt clean.

(Note: the earlier "webm alpha broken on Windows" report from a prior batch was resolved by a later reporter as an ffmpeg-default-decoder confusion, not a code bug — webm VP9 alpha renders correctly; consumers must decode with -c:v libvpx-vp9. Logged as a docs-callout follow-up.)

…s EPERM

materializeExtractedFramesForCompiledDir stages each video's extracted
frames into the compiled dir via a single symlink (the in-process
renderer's default; distributed plan() already copies via
materializeSymlinks). Windows without Developer Mode (or Administrator)
cannot create symlinks and rejects with EPERM, so high/standard-quality
renders failed there — while draft quality worked because it avoids the
symlinked-cache path entirely.

Fix: a new stageExtractedFrameDir helper catches EPERM/EACCES from
symlinkSync and falls back to the same recursive cpSync the
materializeSymlinks path already uses. The extra disk is far better than
a hard render failure on a default Windows configuration. Non-permission
errors (ENOSPC, etc.) still propagate so real failures aren't masked as
silent copies. Extracting the helper also keeps the main function under
the complexity gate.

Test: two new cases via the injected fileSystem — symlinkSync throwing
EPERM triggers exactly one recursive cpSync (frames still remapped under
compiledDir), and a non-permission error (ENOSPC) rethrows without
falling back to copy. Full renderOrchestrator suite (81) passes.
Follow-up to this PR's EPERM copy fallback, from a further Windows report: the
symlink fails with EEXIST after the extraction cache is GC'd. A prior render's
symlink at the compiled linkPath dangles once its target is removed; the
caller's existsSync() guard follows the dead link and reads it as absent, so
staging runs again, but the link file still exists and symlinkSync collides
with EEXIST -> the render hard-fails.

Catch EEXIST in stageExtractedFrameDir, clear the stale entry (rmSync), and
re-stage (link, or copy on EPERM/EACCES). Factored the link-or-copy into a
helper reused by both the first attempt and the retry. rmSync is an optional
injected fs method (default fs supplies it; only the EEXIST path calls it).
New unit test covers the dangling-symlink recovery.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Added a follow-up commit for the EEXIST variant of the same failure, from a further Windows report: after the extraction cache is GC'd, a prior render's frame-symlink at the compiled linkPath dangles. The caller's existsSync() guard follows the dead link and reads it as absent, so staging runs again, but the link file still exists and symlinkSync collides with EEXIST (the reporter also named EEXIST alongside EPERM). stageExtractedFrameDir now catches EEXIST, clears the stale entry (rmSync), and re-stages (link, or copy on EPERM/EACCES). Link-or-copy is factored into a small helper reused by the first attempt and the retry. New unit test covers the dangling-symlink recovery.

@miguel-heygen miguel-heygen changed the title fix(producer): fall back to copying extracted frames when symlink hits EPERM [P1] fix(producer): fall back to copying extracted frames when symlink hits EPERM Jul 7, 2026
@miguel-heygen
miguel-heygen marked this pull request as ready for review July 7, 2026 19:04
@miguel-heygen miguel-heygen changed the title [P1] fix(producer): fall back to copying extracted frames when symlink hits EPERM fix(producer): fall back to copying extracted frames when symlink hits EPERM [P1] Jul 7, 2026
…copy path

Addresses review nits on the frame-staging fallback:
- Widen the symlink no-privilege catch from EPERM/EACCES to also include
  UNKNOWN (some Windows builds surface a symlink privilege denial as UNKNOWN).
- Wrap the EEXIST stale-entry recovery around BOTH staging branches, not just
  the symlink one: after #2025 Windows uses the eager cpSync path, which can
  collide with a dangling symlink left by a prior Linux run — now it clears the
  stale entry and re-stages either way.
- Emit a one-time INFO log when symlinking degrades to copying, so a heavier
  Windows render is self-explanatory.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

@tai thanks — addressed the nits in 5160b0e:

  • UNKNOWN code: the no-privilege catch now includes EPERM | EACCES | UNKNOWN (some Windows builds surface the symlink privilege denial as UNKNOWN).
  • EEXIST on the eager-copy path: moved the stale-entry recovery to wrap both staging branches (via a stageExtractedFrameDirOnce helper), so after fix(producer): copy extracted frames on Windows to avoid symlink EPERM [P0] #2025 routes Windows through eager cpSync, a dir a prior Linux run left with a dangling symlink self-heals (rm + re-stage) instead of colliding. Added a unit test for it.
  • Fallback log: emit a one-time INFO when symlinking degrades to copying, so a heavier Windows render is self-explanatory.
  • Atomic copy-to-tmp+rename: agree it's the durable fix, but as you noted it's pre-existing (the eager-copy shape predates this PR) — leaving it as a tracked follow-up rather than widening scope here.

@miguel-heygen
miguel-heygen merged commit 00b96d2 into main Jul 7, 2026
70 of 71 checks passed
@miguel-heygen
miguel-heygen deleted the fix/frames-symlink-eperm-copy-fallback branch July 7, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant