diff --git a/packages/cli-common/src/run.test.ts b/packages/cli-common/src/run.test.ts index 04df5fe05f..01c6c915d6 100644 --- a/packages/cli-common/src/run.test.ts +++ b/packages/cli-common/src/run.test.ts @@ -122,9 +122,41 @@ describe('run', () => { activeChildren.push(child); // Wait for it to complete await child.waitForExit(); - await expect( - Promise.race([child.waitForExit(), 'pending']), - ).resolves.not.toBe('pending'); + // Call waitForExit again - should return immediately + await expect(child.waitForExit()).resolves.not.toThrow(); + }); + + it('should handle multiple simultaneous calls to waitForExit', async () => { + const child = run(['node', '--version']); + activeChildren.push(child); + // Call waitForExit multiple times simultaneously + const [result1, result2, result3] = await Promise.all([ + child.waitForExit(), + child.waitForExit(), + child.waitForExit(), + ]); + // All should resolve successfully + expect(result1).toBeUndefined(); + expect(result2).toBeUndefined(); + expect(result3).toBeUndefined(); + }); + + it('should handle multiple simultaneous calls to waitForExit with error', async () => { + const child = run(['node', '--eval', 'process.exit(1)']); + activeChildren.push(child); + // Call waitForExit multiple times simultaneously + const promises = [ + child.waitForExit(), + child.waitForExit(), + child.waitForExit(), + ]; + // All should reject with the same error + for (const promise of promises) { + await expect(promise).rejects.toThrow(ExitCodeError); + await expect(promise).rejects.toThrow( + /Command 'node --eval process\.exit\(1\)' exited with code 1/, + ); + } }); it('should handle signal handlers cleanup', async () => { diff --git a/packages/cli-common/src/run.ts b/packages/cli-common/src/run.ts index b64d2bb94b..9bdfbcd1f6 100644 --- a/packages/cli-common/src/run.ts +++ b/packages/cli-common/src/run.ts @@ -99,49 +99,60 @@ export function run(args: string[], options: RunOptions = {}): RunChildProcess { const commandName = args.join(' '); - let signalHandlersRegistered = false; - const handleSignal = () => { - if (!child.killed && child.exitCode === null) { - child.kill(); - } - }; + let waitPromise: Promise | undefined; child.waitForExit = async (): Promise => { - // Register signal handlers to kill child process on SIGINT/SIGTERM - if (!signalHandlersRegistered) { - for (const signal of ['SIGINT', 'SIGTERM'] as const) { - process.on(signal, handleSignal); - } - signalHandlersRegistered = true; + if (waitPromise) { + return waitPromise; } - try { + waitPromise = new Promise((resolve, reject) => { if (typeof child.exitCode === 'number') { if (child.exitCode) { - throw new ExitCodeError(child.exitCode, commandName); + reject(new ExitCodeError(child.exitCode, commandName)); + } else { + resolve(); } return; } - await new Promise((resolve, reject) => { - child.once('error', reject); - child.once('exit', code => { - if (code) { - reject(new ExitCodeError(code, commandName)); - } else { - resolve(); - } - }); - }); - } finally { - // Clean up signal handlers when done waiting - if (signalHandlersRegistered) { - for (const signal of ['SIGINT', 'SIGTERM'] as const) { - process.removeListener(signal, handleSignal); - } - signalHandlersRegistered = false; + function onError(error: Error) { + cleanup(); + reject(error); } - } + + function onExit(code: number | null) { + cleanup(); + if (code) { + reject(new ExitCodeError(code, commandName)); + } else { + resolve(); + } + } + + function onSignal() { + if (!child.killed && child.exitCode === null) { + child.kill(); + } + } + + function cleanup() { + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.removeListener(signal, onSignal); + } + child.removeListener('error', onError); + child.removeListener('exit', onExit); + } + + child.once('error', onError); + child.once('exit', onExit); + + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.addListener(signal, onSignal); + } + }); + + return waitPromise; }; return child;