Skip to content

Commit 24374f1

Browse files
committed
child_process: release the abort listener when a spawn fails
A failed spawn emits 'error' and 'close' but never 'exit'. The AbortSignal cleanup in spawn() was registered on 'exit', so the abort listener stayed attached to the user's signal after a failed spawn. Register the cleanup on 'close' as well, matching the timeout cleanup. Signed-off-by: Abdel <hktitof@gmail.com>
1 parent cede7e6 commit 24374f1

2 files changed

Lines changed: 20 additions & 0 deletions

File tree

‎lib/child_process.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,10 @@ function spawn(file, args, options) {
897897
} else {
898898
addAbortListener ??= require('internal/events/abort_listener').addAbortListener;
899899
const disposable = addAbortListener(signal, onAbortListener);
900+
// 'exit' does not fire when the spawn itself fails, so the listener
901+
// is also released on 'close', which always fires.
900902
child.once('exit', disposable[SymbolDispose]);
903+
child.once('close', disposable[SymbolDispose]);
901904
}
902905

903906
function onAbortListener() {

‎test/parallel/test-child-process-spawn-timeout-clear-on-error.js‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
// The outer spawnSync timeout catches a leaked inner timer.
55

66
const common = require('../common');
7+
const assert = require('node:assert');
8+
const { getEventListeners } = require('node:events');
9+
const { spawn } = require('node:child_process');
710
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
811

912
const bugStallMs = common.platformTimeout(10000);
@@ -17,3 +20,17 @@ spawnSyncAndExitWithoutError(process.execPath, ['-e', `
1720
});
1821
cp.on('error', () => {});
1922
`], { timeout: outerTimeoutMs });
23+
24+
// A failed spawn never emits 'exit'. The abort listener must be gone by
25+
// the time 'close' fires, or it stays attached to the user's signal.
26+
{
27+
const controller = new AbortController();
28+
const cp = spawn(process.execPath, ['--version'], {
29+
cwd: '/nonexistent/path/that/should/never/exist',
30+
signal: controller.signal,
31+
});
32+
cp.on('error', () => {});
33+
cp.on('close', common.mustCall(() => {
34+
assert.strictEqual(getEventListeners(controller.signal, 'abort').length, 0);
35+
}));
36+
}

0 commit comments

Comments
 (0)