cli-common: cleanup and polish waitForExit

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-11-29 23:20:46 +01:00
parent 788ed91649
commit 6359995329
2 changed files with 78 additions and 35 deletions
+35 -3
View File
@@ -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 () => {
+43 -32
View File
@@ -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<void> | undefined;
child.waitForExit = async (): Promise<void> => {
// 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<void>((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<void>((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;