fix(nodecg): include actionable diagnostics when process exits before readiness (#27)

This commit is contained in:
Pandipipas
2026-02-23 22:24:22 +01:00
committed by GitHub
parent 1f7b05e703
commit 4aa75802cc
2 changed files with 64 additions and 2 deletions
+21 -2
View File
@@ -49,6 +49,8 @@ export function createNodecgProcessManager({
let nodecgProcess: ChildProcess | null = null; let nodecgProcess: ChildProcess | null = null;
let stopNodecgPromise: Promise<void> | null = null; let stopNodecgPromise: Promise<void> | null = null;
let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
let lastStderrLine: string | null = null;
const startNodecgProcess = async (): Promise<ChildProcess> => { const startNodecgProcess = async (): Promise<ChildProcess> => {
validateNodecgInstall( validateNodecgInstall(
@@ -85,16 +87,21 @@ export function createNodecgProcessManager({
}); });
child.stderr?.on("data", (chunk) => { child.stderr?.on("data", (chunk) => {
resolvedDeps.stderrWrite(String(chunk)); const line = String(chunk);
lastStderrLine = line.trim().length > 0 ? line.trim() : lastStderrLine;
resolvedDeps.stderrWrite(line);
}); });
log(`NodeCG started with pid=${child.pid} using ${NODE_RUNTIME_NAME}`); log(`NodeCG started with pid=${child.pid} using ${NODE_RUNTIME_NAME}`);
child.on("exit", (code, signal) => { child.on("exit", (code, signal) => {
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`); log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
lastExit = { code, signal };
nodecgProcess = null; nodecgProcess = null;
}); });
lastExit = null;
lastStderrLine = null;
nodecgProcess = child; nodecgProcess = child;
return child; return child;
}; };
@@ -102,7 +109,19 @@ export function createNodecgProcessManager({
const waitForNodecgReady = async (startTime: number): Promise<void> => { const waitForNodecgReady = async (startTime: number): Promise<void> => {
while (Date.now() - startTime < appConfig.startupTimeoutMs) { while (Date.now() - startTime < appConfig.startupTimeoutMs) {
if (!nodecgProcess) { if (!nodecgProcess) {
throw new Error("NodeCG terminó antes de estar listo."); const exitDetails = lastExit
? `Última salida registrada: code=${lastExit.code ?? "null"}, signal=${lastExit.signal ?? "none"}.`
: "No se registró código de salida del proceso NodeCG.";
const stderrDetails = lastStderrLine ? `Último stderr: ${lastStderrLine}` : "Sin salida stderr capturada.";
throw new Error(
[
"NodeCG terminó antes de estar listo.",
exitDetails,
stderrDetails,
`Ruta NodeCG: ${nodecgRootPath}`,
"Revisa que lib/nodecg tenga dependencias instaladas y que el bundle exista.",
].join("\n"),
);
} }
try { try {
+43
View File
@@ -236,3 +236,46 @@ test("startNodeCG falla si el puerto ya está ocupado", async () => {
await manager.startNodecgProcess(); await manager.startNodecgProcess();
}, /ya está en uso/); }, /ya está en uso/);
}); });
test("waitForNodeCGReady expone diagnóstico cuando NodeCG sale antes de readiness", async () => {
const child = new MockChildProcess(4242);
const manager = createNodecgProcessManager({
isDev: true,
nodecgRootPath: "/fake/nodecg",
nodecgBaseUrl: "http://127.0.0.1:9090",
appConfig: getBaseConfig(),
log: () => undefined,
deps: {
pathExists: () => true,
platform: "linux",
spawnProcess: () => child as unknown as import("node:child_process").ChildProcess,
fetchUrl: async () => {
child.emit("exit", 1, null);
throw new Error("still starting");
},
setTimer: (handler) => {
handler();
return 0;
},
stdoutWrite: () => undefined,
stderrWrite: () => undefined,
probePortAvailable: async () => true,
hasReadWriteAccess: () => true,
},
});
await manager.startNodecgProcess();
await assert.rejects(
async () => {
await manager.waitForNodecgReady(Date.now());
},
(error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, /NodeCG terminó antes de estar listo/);
assert.match(error.message, /Última salida registrada/);
assert.match(error.message, /Ruta NodeCG: \/fake\/nodecg/);
return true;
},
);
});