fix(engine): give the ffmpeg children the signal that actually aborts them

The internal AbortController exists so a fatal FX error can cancel in-flight
ffmpeg before the finally-block deletes workDir. It reached the entry guard and
applyAudioFxChain, and nothing else: the trim, the video extract and the
download were all still handed the CALLER's signal. So `internalController
.abort()` cancelled nothing, and the `rmSync(workDir, { recursive: true })`
immediately after it ran while those children were still writing into the
directory.

Concretely: track A's FX render fails while track B is mid-trim, with no
external cancellation. B's ffmpeg keeps writing `${id}-trimmed.wav` into an
unlinked directory until it finishes. The comment above the controller
describes a mechanism that was never wired to the processes it names.

The download also had no signal at all, though downloadToTemp accepts one.

No test: the three changes are which variable is passed to a child process
helper, and observing the difference means driving real ffmpeg to the point of
cancellation. The old wiring's inertness is provable by reading — the caller's
signal is never aborted by this function — and the new wiring is the same three
calls with the controller the function already built.
This commit is contained in:
Vance Ingalls
2026-08-08 11:12:58 -07:00
parent 371920695c
commit bfa27381e1
+9 -3
View File
@@ -765,6 +765,12 @@ export async function processCompositionAudio(
// be able to abort the in-flight ffmpeg runs before the finally-block removes
// workDir out from under them. Chained off the caller's signal so external
// cancellation still behaves as before.
//
// Every child that can outlive a sibling's failure has to be given THIS
// signal, not the caller's: the trim, the video extract and the download all
// took `signal`, so `internalController.abort()` cancelled nothing and the
// `rmSync(workDir)` on the next line ran while their ffmpeg children were
// still writing into it.
const internalController = new AbortController();
const effectiveSignal = internalController.signal;
if (signal) {
@@ -795,7 +801,7 @@ export async function processCompositionAudio(
if (isHttpUrl(srcPath)) {
try {
srcPath = await downloadToTemp(srcPath, workDir);
srcPath = await downloadToTemp(srcPath, workDir, undefined, effectiveSignal);
} catch (err: unknown) {
failures.push(
downloadFailure(err instanceof Error ? err.message : String(err), element.id),
@@ -842,7 +848,7 @@ export async function processCompositionAudio(
startTime: element.mediaStart,
duration: element.end - element.start,
},
signal,
effectiveSignal,
config,
);
if (!extractResult.success) {
@@ -868,7 +874,7 @@ export async function processCompositionAudio(
trimmedPath,
element.mediaStart,
element.end - element.start,
signal,
effectiveSignal,
config,
);
if (!prepResult.success) {