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:
cxbAsDev
2026-07-04 13:49:12 +08:00
committed by GitHub
parent 1b84316a91
commit 3b37fe1de4
2 changed files with 43 additions and 0 deletions
@@ -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 });
});
});
+5
View File
@@ -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;