mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +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:
@@ -131,6 +131,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Webchat/security: reject remote-host `file://` URLs in the media embedding path. (#67293) Thanks @pgondhi987.
|
||||
- Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9.
|
||||
- Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.
|
||||
- Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf
|
||||
|
||||
## 2026.4.14
|
||||
|
||||
|
||||
@@ -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