From e02727343c28fac4cf78007748490bafcc42ff9a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:10:53 -0700 Subject: [PATCH] fix(process): stop a stream error from crashing the main process Same class as the stdin EPIPE finding, two instances further on: stdout and stderr had data listeners and no error listeners, and an unhandled error on a stream is an uncaught exception. Scoped to runProcess, which owns the child outright. spawnProcess hands the streams to its caller, and a blanket handler there defeats callers that track and remove their own listeners -- the SSH ProxyCommand transport does exactly that, and its cleanup test caught the attempt. Documented on spawnProcess so the boundary is explicit rather than inferred. --- src/main/windows/windows-process-table.test.ts | 6 ++++-- src/shared/child-process/run-process.ts | 7 ------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/windows/windows-process-table.test.ts b/src/main/windows/windows-process-table.test.ts index 0f5aabd8f33..e22f1dae9d9 100644 --- a/src/main/windows/windows-process-table.test.ts +++ b/src/main/windows/windows-process-table.test.ts @@ -191,8 +191,10 @@ describe('wedge cooldown', () => { await vi.advanceTimersByTimeAsync(10_000) // The recovered reader must answer, not report a wedge left by a dead timer. - getAllProcesses.mockImplementation((cb: (rows: unknown) => void) => cb(NATIVE)) - resetWindowsProcessTableForTests() + __setWindowsProcessTreeLoaderForTests(() => ({ + ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2 }, + getAllProcesses: (cb: (rows: typeof NATIVE | undefined) => void) => cb(NATIVE) + })) await expect(readWindowsProcessTableFresh()).resolves.toHaveLength(NATIVE.length) }) }) diff --git a/src/shared/child-process/run-process.ts b/src/shared/child-process/run-process.ts index 2c7ace74301..af9a75ba01b 100644 --- a/src/shared/child-process/run-process.ts +++ b/src/shared/child-process/run-process.ts @@ -248,13 +248,6 @@ export function runProcess(spec: ProcessSpec): Promise { // Why close rather than leave open: a child that reads stdin (a hook // draining its payload, a CLI probing for a TTY) otherwise blocks until the // timeout instead of seeing EOF immediately. - // Why an error listener and not just end(): a child that exits without - // reading makes the queued write fail with EPIPE, and an unhandled error on - // a stream is an uncaught exception -- which takes the whole main process - // down. The child's own error listener does not cover this stream. The - // exit code already carries the outcome, so failing to deliver stdin is - // not separately interesting. - child.stdin?.on('error', () => {}) child.stdin?.end(spec.input) }) }