diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts index 747337a26..1b461e0a2 100644 --- a/packages/core/src/services/shellExecutionService.test.ts +++ b/packages/core/src/services/shellExecutionService.test.ts @@ -438,6 +438,8 @@ describe('ShellExecutionService child_process fallback', () => { configurable: true, }); + mockChildProcess.once = mockChildProcess.on.bind(mockChildProcess); + mockCpSpawn.mockReturnValue(mockChildProcess); }); @@ -491,6 +493,16 @@ describe('ShellExecutionService child_process fallback', () => { }); }); + it('should resolve when only the close event fires', async () => { + const { result } = await simulateExecution('ls -l', (cp) => { + cp.stdout?.emit('data', Buffer.from('file1.txt\n')); + cp.emit('close', 0, null); + }); + + expect(result.exitCode).toBe(0); + expect(result.output).toBe('file1.txt'); + }); + it('should strip ANSI codes from output', async () => { const { result } = await simulateExecution('ls --color=auto', (cp) => { cp.stdout?.emit('data', Buffer.from('a\u001b[31mred\u001b[0mword')); @@ -831,6 +843,7 @@ describe('ShellExecutionService execution method selection', () => { value: 54321, configurable: true, }); + mockChildProcess.once = mockChildProcess.on.bind(mockChildProcess); mockCpSpawn.mockReturnValue(mockChildProcess); }); diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index d01302b70..ed851c1c8 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -263,10 +263,16 @@ export class ShellExecutionService { } }; + let hasResolved = false; + const handleExit = ( code: number | null, signal: NodeJS.Signals | null, ) => { + if (hasResolved) { + return; + } + hasResolved = true; const { finalBuffer } = cleanup(); // Ensure we don't add an extra newline if stdout already ends with one. const separator = stdout.endsWith('\n') ? '' : '\n'; @@ -321,9 +327,21 @@ export class ShellExecutionService { abortSignal.addEventListener('abort', abortHandler, { once: true }); - child.on('exit', (code, signal) => { - handleExit(code, signal); - }); + if (child.once) { + child.once('exit', (code, signal) => { + handleExit(code, signal); + }); + child.once('close', (code, signal) => { + handleExit(code, signal); + }); + } else { + child.on('exit', (code, signal) => { + handleExit(code, signal); + }); + child.on('close', (code, signal) => { + handleExit(code, signal); + }); + } function cleanup() { exited = true; diff --git a/packages/core/src/services/shellExecutionService.windows.test.ts b/packages/core/src/services/shellExecutionService.windows.test.ts index 14c47d106..1eecda3d0 100644 --- a/packages/core/src/services/shellExecutionService.windows.test.ts +++ b/packages/core/src/services/shellExecutionService.windows.test.ts @@ -18,6 +18,7 @@ const fakeChildFactory = () => { stdout: { on: vi.fn<(event: string, cb: Listener) => void>() }, stderr: { on: vi.fn<(event: string, cb: Listener) => void>() }, on: vi.fn<(event: string, cb: Listener) => void>(), + once: vi.fn<(event: string, cb: Listener) => void>(), pid: 2222, kill: vi.fn<(signal?: NodeJS.Signals) => boolean>(), };