mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(supervisor): suppress unhandled stream errors on child stdout/stderr (#99802)
* fix(supervisor): suppress unhandled stream errors on child stdout/stderr Child process stdout and stderr streams can emit 'error' events (EPIPE, resource limits, I/O errors) that crash the process when no listener is attached. Add noop error handlers to prevent crashes while preserving normal data/end/close event flow. Co-Authored-By: Claude <noreply@anthropic.com> * test(supervisor): add regression test for stream error suppression Co-Authored-By: Claude <noreply@anthropic.com> * fix(supervisor): guard child stream errors during setup --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -511,4 +511,42 @@ describe("createChildAdapter", () => {
|
||||
expect(first).toHaveBeenCalledWith("first");
|
||||
expect(second).toHaveBeenCalledWith("second");
|
||||
});
|
||||
|
||||
it("guards stream errors before output listeners are registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
setPlatform("win32");
|
||||
const { child, emitExit } = createStubChild(6666);
|
||||
spawnWithFallbackMock.mockResolvedValue({
|
||||
child,
|
||||
usedFallback: false,
|
||||
});
|
||||
const adapter = await createChildAdapter({
|
||||
argv: ["node", "-e", "setTimeout(() => {}, 1000)"],
|
||||
stdinMode: "pipe-open",
|
||||
});
|
||||
|
||||
const stdoutErr = new Error("simulated stdout pipe error");
|
||||
const stderrErr = new Error("simulated stderr pipe error");
|
||||
const settled = vi.fn();
|
||||
void adapter.wait().then(settled);
|
||||
|
||||
emitExit(0, null);
|
||||
expect(() => child.stdout?.emit("error", stdoutErr)).not.toThrow();
|
||||
expect(() => child.stderr?.emit("error", stderrErr)).not.toThrow();
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
expect(settled).not.toHaveBeenCalled();
|
||||
|
||||
adapter.onStdout(() => {});
|
||||
adapter.onStdout(() => {});
|
||||
adapter.onStderr(() => {});
|
||||
adapter.onStderr(() => {});
|
||||
|
||||
expect(child.stdout?.listenerCount("error")).toBe(1);
|
||||
expect(child.stderr?.listenerCount("error")).toBe(1);
|
||||
|
||||
child.stdout?.emit("close");
|
||||
child.stderr?.emit("close");
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(settled).toHaveBeenCalledWith({ code: 0, signal: null });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,6 +103,11 @@ export async function createChildAdapter(params: {
|
||||
});
|
||||
|
||||
const child = spawned.child as ChildProcessWithoutNullStreams;
|
||||
// Pipe errors can arrive before output subscribers attach. Close remains
|
||||
// responsible for decoder flush and Windows drain completion.
|
||||
const ignoreOutputStreamError = () => {};
|
||||
child.stdout.on("error", ignoreOutputStreamError);
|
||||
child.stderr.on("error", ignoreOutputStreamError);
|
||||
const childStdin = spawned.child.stdin;
|
||||
let stdinDestroyed = childStdin?.destroyed ?? false;
|
||||
let stdinEnded = childStdin?.writableEnded === true || childStdin?.writableFinished === true;
|
||||
|
||||
Reference in New Issue
Block a user