From cb08a42d0da8f8e3d8a2563bf7651739f1c5d3c8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 08:38:54 -0700 Subject: [PATCH] fix: clean up command runner capture listeners --- src/main/git/runner-command-exec.test.ts | 28 ++++++++++++++++++ src/main/git/runner.ts | 36 ++++++++++++++++-------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/main/git/runner-command-exec.test.ts b/src/main/git/runner-command-exec.test.ts index 83a326b019e..688ea72cef0 100644 --- a/src/main/git/runner-command-exec.test.ts +++ b/src/main/git/runner-command-exec.test.ts @@ -112,6 +112,10 @@ describe('commandExecFileAsync Windows command shims', () => { expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ) expect(command.kill).not.toHaveBeenCalled() + expect(command.stdout.listenerCount('data')).toBe(0) + expect(command.stderr.listenerCount('data')).toBe(0) + expect(command.listenerCount('error')).toBe(0) + expect(command.listenerCount('close')).toBe(0) }) }) @@ -137,6 +141,30 @@ describe('commandExecFileAsync Windows command shims', () => { expect.objectContaining({ stdio: 'ignore', windowsHide: true }) ) expect(command.kill).not.toHaveBeenCalled() + expect(command.stdout.listenerCount('data')).toBe(0) + expect(command.stderr.listenerCount('data')).toBe(0) + expect(command.listenerCount('error')).toBe(0) + expect(command.listenerCount('close')).toBe(0) + }) + }) + + it('removes listeners after successful Windows .cmd shim executions', async () => { + await withPlatform('win32', async () => { + const command = createMockChildProcess(1234) + spawnMock.mockReturnValue(command) + + const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['--version'], { + cwd: 'C:\\repo' + }) + command.stdout.emit('data', Buffer.from('9.1.0\n')) + command.stderr.emit('data', Buffer.from('notice\n')) + command.emit('close', 0) + + await expect(promise).resolves.toEqual({ stdout: '9.1.0\n', stderr: 'notice\n' }) + expect(command.stdout.listenerCount('data')).toBe(0) + expect(command.stderr.listenerCount('data')).toBe(0) + expect(command.listenerCount('error')).toBe(0) + expect(command.listenerCount('close')).toBe(0) }) }) }) diff --git a/src/main/git/runner.ts b/src/main/git/runner.ts index e7dbc07e76e..3e404cebdc7 100644 --- a/src/main/git/runner.ts +++ b/src/main/git/runner.ts @@ -296,15 +296,23 @@ async function spawnCommandCapture( killSpawnedCommandTree(child) finish(createAbortError()) } + const cleanupListeners = (): void => { + if (timer) { + clearTimeout(timer) + timer = null + } + options.signal?.removeEventListener('abort', onAbort) + child.stdout?.off('data', onStdoutData) + child.stderr?.off('data', onStderrData) + child.off('error', onError) + child.off('close', onClose) + } const finish = (error: Error | null): void => { if (settled) { return } settled = true - if (timer) { - clearTimeout(timer) - } - options.signal?.removeEventListener('abort', onAbort) + cleanupListeners() if (error) { reject(Object.assign(error, { stdout, stderr })) return @@ -318,7 +326,7 @@ async function spawnCommandCapture( }, options.timeout) : null options.signal?.addEventListener('abort', onAbort, { once: true }) - child.stdout?.on('data', (chunk: Buffer) => { + function onStdoutData(chunk: Buffer): void { stdoutBytes += chunk.byteLength if (options.maxBuffer && stdoutBytes > options.maxBuffer) { killSpawnedCommandTree(child) @@ -326,8 +334,8 @@ async function spawnCommandCapture( return } stdout += chunk.toString(options.encoding ?? 'utf-8') - }) - child.stderr?.on('data', (chunk: Buffer) => { + } + function onStderrData(chunk: Buffer): void { stderrBytes += chunk.byteLength if (options.maxBuffer && stderrBytes > options.maxBuffer) { killSpawnedCommandTree(child) @@ -335,15 +343,21 @@ async function spawnCommandCapture( return } stderr += chunk.toString(options.encoding ?? 'utf-8') - }) - child.on('error', finish) - child.on('close', (code) => { + } + function onError(error: Error): void { + finish(error) + } + function onClose(code: number | null): void { if (code === 0) { finish(null) return } finish(new Error(`${command} exited with ${code}.`)) - }) + } + child.stdout?.on('data', onStdoutData) + child.stderr?.on('data', onStderrData) + child.on('error', onError) + child.on('close', onClose) }) }