fix(server): handle codex spawn errors to prevent daemon crash
Spawning a missing/broken codex binary emits an async "error" event on
the child process. Without a listener, Node.js crashes the daemon with
no log entry. Add child.on("error") in CodexAppServerClient and global
uncaughtException/unhandledRejection handlers that log via pino before
exiting.
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
||||||
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
|
||||||
|
describe("CodexAppServerAgentClient spawn error handling", () => {
|
||||||
|
const logger = createTestLogger();
|
||||||
|
|
||||||
|
test("listModels rejects gracefully when the codex binary does not exist", async () => {
|
||||||
|
const client = new CodexAppServerAgentClient(logger, {
|
||||||
|
command: {
|
||||||
|
mode: "replace",
|
||||||
|
argv: ["/nonexistent/codex-binary-that-does-not-exist"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const uncaughtErrors: unknown[] = [];
|
||||||
|
const onUncaught = (err: unknown) => {
|
||||||
|
uncaughtErrors.push(err);
|
||||||
|
};
|
||||||
|
process.on("uncaughtException", onUncaught);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(client.listModels()).rejects.toThrow();
|
||||||
|
// Drain microtask queue to ensure no deferred uncaught errors
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
expect(uncaughtErrors).toHaveLength(0);
|
||||||
|
} finally {
|
||||||
|
process.off("uncaughtException", onUncaught);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("listPersistedAgents rejects gracefully when the codex binary does not exist", async () => {
|
||||||
|
const client = new CodexAppServerAgentClient(logger, {
|
||||||
|
command: {
|
||||||
|
mode: "replace",
|
||||||
|
argv: ["/nonexistent/codex-binary-that-does-not-exist"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const uncaughtErrors: unknown[] = [];
|
||||||
|
const onUncaught = (err: unknown) => {
|
||||||
|
uncaughtErrors.push(err);
|
||||||
|
};
|
||||||
|
process.on("uncaughtException", onUncaught);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(client.listPersistedAgents()).rejects.toThrow();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
expect(uncaughtErrors).toHaveLength(0);
|
||||||
|
} finally {
|
||||||
|
process.off("uncaughtException", onUncaught);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -511,6 +511,18 @@ class CodexAppServerClient {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
child.on("error", (err) => {
|
||||||
|
this.logger.error({ err }, "Codex app-server child process error");
|
||||||
|
for (const pending of this.pending.values()) {
|
||||||
|
clearTimeout(pending.timer);
|
||||||
|
pending.reject(err);
|
||||||
|
}
|
||||||
|
this.pending.clear();
|
||||||
|
this.disposed = true;
|
||||||
|
this.resolveExitPromise?.();
|
||||||
|
this.resolveExitPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
child.on("exit", (code, signal) => {
|
||||||
const message =
|
const message =
|
||||||
code === 0 && !signal
|
code === 0 && !signal
|
||||||
|
|||||||
@@ -158,6 +158,16 @@ async function main() {
|
|||||||
|
|
||||||
process.on("SIGTERM", () => beginShutdown("SIGTERM"));
|
process.on("SIGTERM", () => beginShutdown("SIGTERM"));
|
||||||
process.on("SIGINT", () => beginShutdown("SIGINT"));
|
process.on("SIGINT", () => beginShutdown("SIGINT"));
|
||||||
|
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
logger.fatal({ err }, "Uncaught exception — daemon crashing");
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
logger.fatal({ err: reason }, "Unhandled promise rejection — daemon crashing");
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user