mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(producer): guard fileServer.close in all render cleanup paths (#1406)
* fix(producer): guard fileServer.close in distributed cleanup paths `plan()` and `renderChunk()` both close the probe/chunk file server with a bare `fileServer.close()` in their cleanup sequence. `FileServerHandle.close` tears down the underlying http.Server, whose `close()` throws `ERR_SERVER_NOT_RUNNING` if the server was already torn down (for example a cancellation path that closed it once already). An unguarded throw there escapes the cleanup and masks the original plan/render result, exactly the failure the adjacent probe-session close already guards against with a try/catch (its comment even spells this out). Add `closeFileServerSafely`, which wraps the close in a try/catch and logs, and route both cleanup sites through it so the two stay consistent and a throwing close can never mask the real result. Covered by unit tests for both the throwing and happy paths. * fix(producer): extend fileServer.close guard to renderOrchestrator success path --------- Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos Alcaraz
parent
8f71378185
commit
1f9c70cf15
@@ -38,6 +38,7 @@ import { join, relative, sep } from "node:path";
|
||||
import { type CanvasResolution } from "@hyperframes/core";
|
||||
import { type EngineConfig, getEncoderPreset, resolveConfig } from "@hyperframes/engine";
|
||||
import { defaultLogger, type ProducerLogger } from "../../logger.js";
|
||||
import { closeFileServerSafely } from "../fileServer.js";
|
||||
import { runAudioStage } from "../render/stages/audioStage.js";
|
||||
import { runCompileStage } from "../render/stages/compileStage.js";
|
||||
import { runExtractVideosStage } from "../render/stages/extractVideosStage.js";
|
||||
@@ -833,7 +834,7 @@ export async function plan(
|
||||
job.duration = probeResult.duration;
|
||||
job.totalFrames = probeResult.totalFrames;
|
||||
const totalFrames = probeResult.totalFrames;
|
||||
if (probeResult.fileServer) probeResult.fileServer.close();
|
||||
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
||||
if (probeResult.probeSession) {
|
||||
// Close inside a try/catch — leaking a Chrome process here would mask
|
||||
// the original plan() result on cancellation paths.
|
||||
|
||||
@@ -65,7 +65,12 @@ import {
|
||||
} from "../render/stages/freezePlan.js";
|
||||
import { sha256Hex } from "../render/stages/planHash.js";
|
||||
import { applyRuntimeEnvSnapshot } from "../render/runtimeEnvSnapshot.js";
|
||||
import { buildVirtualTimeShim, createFileServer, type FileServerHandle } from "../fileServer.js";
|
||||
import {
|
||||
buildVirtualTimeShim,
|
||||
closeFileServerSafely,
|
||||
createFileServer,
|
||||
type FileServerHandle,
|
||||
} from "../fileServer.js";
|
||||
import {
|
||||
buildSyntheticRenderJob,
|
||||
type DistributedFormat,
|
||||
@@ -654,7 +659,7 @@ export async function renderChunk(
|
||||
});
|
||||
}
|
||||
}
|
||||
fileServer.close();
|
||||
closeFileServerSafely(fileServer, "renderChunk", log);
|
||||
// Leave the temp work dir on failure (helps debugging); remove it on
|
||||
// success below.
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node
|
||||
import path, { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
closeFileServerSafely,
|
||||
createFileServer,
|
||||
HF_BRIDGE_SCRIPT,
|
||||
HF_EARLY_STUB,
|
||||
@@ -11,6 +12,45 @@ import {
|
||||
VIRTUAL_TIME_SHIM,
|
||||
} from "./fileServer.js";
|
||||
|
||||
function captureLogger() {
|
||||
const warnings: { message: string; meta?: Record<string, unknown> }[] = [];
|
||||
return {
|
||||
warnings,
|
||||
log: {
|
||||
error() {},
|
||||
warn(message: string, meta?: Record<string, unknown>) {
|
||||
warnings.push({ message, meta });
|
||||
},
|
||||
info() {},
|
||||
debug() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("closeFileServerSafely", () => {
|
||||
it("swallows and logs a throwing close instead of propagating", () => {
|
||||
const { log, warnings } = captureLogger();
|
||||
const fileServer = {
|
||||
close: () => {
|
||||
// http.Server.close() throws ERR_SERVER_NOT_RUNNING on a second close.
|
||||
throw new Error("Server is not running.");
|
||||
},
|
||||
};
|
||||
expect(() => closeFileServerSafely(fileServer, "plan", log)).not.toThrow();
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0].message).toContain("[plan]");
|
||||
expect(warnings[0].meta?.error).toBe("Server is not running.");
|
||||
});
|
||||
|
||||
it("closes once and stays quiet on the happy path", () => {
|
||||
const { log, warnings } = captureLogger();
|
||||
let closed = 0;
|
||||
closeFileServerSafely({ close: () => closed++ }, "renderChunk", log);
|
||||
expect(closed).toBe(1);
|
||||
expect(warnings).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("injectScriptsIntoHtml", () => {
|
||||
it("injects the virtual time shim into head content before authored scripts", () => {
|
||||
const html = `<!DOCTYPE html>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { join, extname, resolve, sep } from "node:path";
|
||||
import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
|
||||
import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js";
|
||||
import { getHfEarlyStub } from "../generated/hf-early-stub-inline.js";
|
||||
import { defaultLogger, type ProducerLogger } from "../logger.js";
|
||||
|
||||
export { injectScriptsAtHeadStart };
|
||||
|
||||
@@ -558,6 +559,30 @@ export interface FileServerHandle {
|
||||
addPreHeadScript: (script: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a file server handle, swallowing and logging any error.
|
||||
*
|
||||
* `FileServerHandle.close` tears down the underlying http.Server, whose
|
||||
* `close()` throws `ERR_SERVER_NOT_RUNNING` if the server is already torn down
|
||||
* (for example a cancellation path that closed it once already). An unguarded
|
||||
* throw inside a cleanup or `finally` block would mask the original render or
|
||||
* plan result, so cleanup callers must go through this instead of calling
|
||||
* `close()` directly.
|
||||
*/
|
||||
export function closeFileServerSafely(
|
||||
fileServer: Pick<FileServerHandle, "close">,
|
||||
label: string,
|
||||
log: ProducerLogger = defaultLogger,
|
||||
): void {
|
||||
try {
|
||||
fileServer.close();
|
||||
} catch (err) {
|
||||
log.warn(`[${label}] file server close failed`, {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createFileServer(options: FileServerOptions): Promise<FileServerHandle> {
|
||||
const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import { join, dirname, resolve } from "path";
|
||||
import { randomUUID } from "crypto";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
closeFileServerSafely,
|
||||
createFileServer,
|
||||
type FileServerHandle,
|
||||
HF_PAGE_SIDE_COMPOSITING_STUB,
|
||||
@@ -1567,7 +1568,7 @@ export async function executeRenderJob(
|
||||
if (frameLookup) frameLookup.cleanup();
|
||||
|
||||
// Stop file server
|
||||
fileServer.close();
|
||||
closeFileServerSafely(fileServer, "renderOrchestrator", log);
|
||||
fileServer = null;
|
||||
|
||||
// ── Stage 6: Assemble ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user