mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:50:45 +00:00
fix(codex): prevent gateway crash when app-server subprocess terminates abruptly (#67947)
Fixes openclaw#67886. Handles stdin EPIPE in CodexAppServerClient by attaching an error handler, guarding writeMessage against writes after close, and aligning closeWithError cleanup with close.
This commit is contained in:
@@ -199,6 +199,39 @@ describe("CodexAppServerClient", () => {
|
||||
expect(process.kill).toHaveBeenCalledWith("SIGKILL");
|
||||
expect(process.unref).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("handles stdin write errors without crashing the process", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
// Start a pending request so we can verify it gets properly rejected.
|
||||
const pending = harness.client.request("test/method");
|
||||
|
||||
// Simulate the child process closing its pipe — a write to the now-dead
|
||||
// stdin emits an asynchronous EPIPE error on the stream.
|
||||
harness.process.stdin.destroy(Object.assign(new Error("write EPIPE"), { code: "EPIPE" }));
|
||||
|
||||
// The pending request must be rejected with the pipe error rather than
|
||||
// an unhandled exception tearing down the gateway.
|
||||
await expect(pending).rejects.toThrow("write EPIPE");
|
||||
|
||||
// Subsequent requests are rejected immediately (client is closed).
|
||||
await expect(harness.client.request("another/method")).rejects.toThrow(
|
||||
"codex app-server client is closed",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not write to stdin after the child process exits", async () => {
|
||||
const harness = createClientHarness();
|
||||
clients.push(harness.client);
|
||||
|
||||
// Simulate the child process exiting.
|
||||
harness.process.emit("exit", 1, null);
|
||||
|
||||
// A notification after exit must not attempt a write.
|
||||
harness.client.notify("late/event", { data: "ignored" });
|
||||
expect(harness.writes).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("reads the Codex version from the app-server user agent", () => {
|
||||
expect(readCodexVersionFromUserAgent("Codex Desktop/0.118.0")).toBe("0.118.0");
|
||||
expect(readCodexVersionFromUserAgent("openclaw/0.118.0 (macOS; test)")).toBe("0.118.0");
|
||||
|
||||
@@ -74,6 +74,13 @@ export class CodexAppServerClient {
|
||||
),
|
||||
);
|
||||
});
|
||||
// Guard against unhandled EPIPE / write-after-close errors on the stdin
|
||||
// stream. When the child process terminates abruptly the pipe can break
|
||||
// before the "exit" event fires, so a pending writeMessage() produces an
|
||||
// asynchronous error on stdin that would otherwise crash the gateway.
|
||||
child.stdin.on?.("error", (error) =>
|
||||
this.closeWithError(error instanceof Error ? error : new Error(String(error))),
|
||||
);
|
||||
}
|
||||
|
||||
static start(options?: Partial<CodexAppServerStartOptions>): CodexAppServerClient {
|
||||
@@ -212,6 +219,9 @@ export class CodexAppServerClient {
|
||||
}
|
||||
|
||||
private writeMessage(message: RpcRequest | RpcResponse): void {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
||||
}
|
||||
|
||||
@@ -300,7 +310,9 @@ export class CodexAppServerClient {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
this.lines.close();
|
||||
this.rejectPendingRequests(error);
|
||||
closeCodexAppServerTransport(this.child);
|
||||
}
|
||||
|
||||
private rejectPendingRequests(error: Error): void {
|
||||
|
||||
@@ -4,6 +4,7 @@ export type CodexAppServerTransport = {
|
||||
end?: () => unknown;
|
||||
destroy?: () => unknown;
|
||||
unref?: () => unknown;
|
||||
on?: (event: "error", listener: (error: Error) => void) => unknown;
|
||||
};
|
||||
stdout: NodeJS.ReadableStream & {
|
||||
destroy?: () => unknown;
|
||||
|
||||
Reference in New Issue
Block a user