Skip to content

Commit a968528

Browse files
authored
fix(supervisor): guard child stream errors during setup
1 parent 1d11f76 commit a968528

2 files changed

Lines changed: 27 additions & 11 deletions

File tree

src/process/supervisor/adapters/child.test.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,10 @@ describe("createChildAdapter", () => {
512512
expect(second).toHaveBeenCalledWith("second");
513513
});
514514

515-
it("suppresses stream errors on stdout and stderr to prevent unhandled error crashes", async () => {
516-
const { child } = createStubChild(6666);
515+
it("guards stream errors before output listeners are registered", async () => {
516+
vi.useFakeTimers();
517+
setPlatform("win32");
518+
const { child, emitExit } = createStubChild(6666);
517519
spawnWithFallbackMock.mockResolvedValue({
518520
child,
519521
usedFallback: false,
@@ -523,17 +525,28 @@ describe("createChildAdapter", () => {
523525
stdinMode: "pipe-open",
524526
});
525527

526-
// Register stdout/stderr listeners to install the error handlers
527-
adapter.onStdout(() => {});
528-
adapter.onStderr(() => {});
529-
530-
// Emitting errors on the streams must not crash the process.
531-
// If the noop error handlers were missing, Node would throw
532-
// an unhandled 'error' event and crash the test runner.
533528
const stdoutErr = new Error("simulated stdout pipe error");
534529
const stderrErr = new Error("simulated stderr pipe error");
530+
const settled = vi.fn();
531+
void adapter.wait().then(settled);
535532

533+
emitExit(0, null);
536534
expect(() => child.stdout?.emit("error", stdoutErr)).not.toThrow();
537535
expect(() => child.stderr?.emit("error", stderrErr)).not.toThrow();
536+
await vi.advanceTimersByTimeAsync(300);
537+
expect(settled).not.toHaveBeenCalled();
538+
539+
adapter.onStdout(() => {});
540+
adapter.onStdout(() => {});
541+
adapter.onStderr(() => {});
542+
adapter.onStderr(() => {});
543+
544+
expect(child.stdout?.listenerCount("error")).toBe(1);
545+
expect(child.stderr?.listenerCount("error")).toBe(1);
546+
547+
child.stdout?.emit("close");
548+
child.stderr?.emit("close");
549+
await vi.advanceTimersByTimeAsync(0);
550+
expect(settled).toHaveBeenCalledWith({ code: 0, signal: null });
538551
});
539552
});

src/process/supervisor/adapters/child.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ export async function createChildAdapter(params: {
103103
});
104104

105105
const child = spawned.child as ChildProcessWithoutNullStreams;
106+
// Pipe errors can arrive before output subscribers attach. Close remains
107+
// responsible for decoder flush and Windows drain completion.
108+
const ignoreOutputStreamError = () => {};
109+
child.stdout.on("error", ignoreOutputStreamError);
110+
child.stderr.on("error", ignoreOutputStreamError);
106111
const childStdin = spawned.child.stdin;
107112
let stdinDestroyed = childStdin?.destroyed ?? false;
108113
let stdinEnded = childStdin?.writableEnded === true || childStdin?.writableFinished === true;
@@ -191,7 +196,6 @@ export async function createChildAdapter(params: {
191196
listener(text);
192197
}
193198
});
194-
child.stdout.on("error", () => {});
195199
child.stdout.once("end", flush);
196200
child.stdout.once("close", flush);
197201
};
@@ -215,7 +219,6 @@ export async function createChildAdapter(params: {
215219
listener(text);
216220
}
217221
});
218-
child.stderr.on("error", () => {});
219222
child.stderr.once("end", flush);
220223
child.stderr.once("close", flush);
221224
};

0 commit comments

Comments
 (0)