diff --git a/src/main/nodecg/process-manager.ts b/src/main/nodecg/process-manager.ts index b463fbe..6c9db98 100644 --- a/src/main/nodecg/process-manager.ts +++ b/src/main/nodecg/process-manager.ts @@ -49,6 +49,8 @@ export function createNodecgProcessManager({ let nodecgProcess: ChildProcess | null = null; let stopNodecgPromise: Promise | null = null; + let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null; + let lastStderrLine: string | null = null; const startNodecgProcess = async (): Promise => { validateNodecgInstall( @@ -85,16 +87,21 @@ export function createNodecgProcessManager({ }); 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}`); child.on("exit", (code, signal) => { log(`NodeCG exited code=${code} signal=${signal ?? "none"}`); + lastExit = { code, signal }; nodecgProcess = null; }); + lastExit = null; + lastStderrLine = null; nodecgProcess = child; return child; }; @@ -102,7 +109,19 @@ export function createNodecgProcessManager({ const waitForNodecgReady = async (startTime: number): Promise => { while (Date.now() - startTime < appConfig.startupTimeoutMs) { 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 { diff --git a/src/tests/process-manager.test.ts b/src/tests/process-manager.test.ts index 013d802..579b5fb 100644 --- a/src/tests/process-manager.test.ts +++ b/src/tests/process-manager.test.ts @@ -236,3 +236,46 @@ test("startNodeCG falla si el puerto ya está ocupado", async () => { await manager.startNodecgProcess(); }, /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; + }, + ); +});