Skip to content

Commit be634c6

Browse files
Nitzan Uzielytargos
authored andcommitted
child_process: add timeout to spawn and fork
Add support for timeout to spawn and fork. Fixes: nodejs#27639 PR-URL: nodejs#37256 Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 6ca23f0 commit be634c6

File tree

4 files changed

+143
-9
lines changed

4 files changed

+143
-9
lines changed

doc/api/child_process.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,9 @@ controller.abort();
374374
<!-- YAML
375375
added: v0.5.0
376376
changes:
377+
- version: REPLACEME
378+
pr-url: https://github.com/nodejs/node/pull/37256
379+
description: timeout was added.
377380
- version: REPLACEME
378381
pr-url: https://github.com/nodejs/node/pull/37325
379382
description: killSignal for AbortSignal was added.
@@ -410,8 +413,8 @@ changes:
410413
See [Advanced serialization][] for more details. **Default:** `'json'`.
411414
* `signal` {AbortSignal} Allows closing the child process using an
412415
AbortSignal.
413-
* `killSignal` {string} The signal value to be used when the spawned
414-
process will be killed by the abort signal. **Default:** `'SIGTERM'`.
416+
* `killSignal` {string|integer} The signal value to be used when the spawned
417+
process will be killed by timeout or abort signal. **Default:** `'SIGTERM'`.
415418
* `silent` {boolean} If `true`, stdin, stdout, and stderr of the child will be
416419
piped to the parent, otherwise they will be inherited from the parent, see
417420
the `'pipe'` and `'inherit'` options for [`child_process.spawn()`][]'s
@@ -423,6 +426,8 @@ changes:
423426
* `uid` {number} Sets the user identity of the process (see setuid(2)).
424427
* `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is
425428
done on Windows. Ignored on Unix. **Default:** `false`.
429+
* `timeout` {number} In milliseconds the maximum amount of time the process
430+
is allowed to run. **Default:** `undefined`.
426431
* Returns: {ChildProcess}
427432

428433
The `child_process.fork()` method is a special case of
@@ -478,6 +483,9 @@ if (process.argv[2] === 'child') {
478483
<!-- YAML
479484
added: v0.1.90
480485
changes:
486+
- version: REPLACEME
487+
pr-url: https://github.com/nodejs/node/pull/37256
488+
description: timeout was added.
481489
- version: REPLACEME
482490
pr-url: https://github.com/nodejs/node/pull/37325
483491
description: killSignal for AbortSignal was added.
@@ -528,8 +536,10 @@ changes:
528536
normally be created on Windows systems. **Default:** `false`.
529537
* `signal` {AbortSignal} allows aborting the child process using an
530538
AbortSignal.
531-
* `killSignal` {string} The signal value to be used when the spawned
532-
process will be killed by the abort signal. **Default:** `'SIGTERM'`.
539+
* `timeout` {number} In milliseconds the maximum amount of time the process
540+
is allowed to run. **Default:** `undefined`.
541+
* `killSignal` {string|integer} The signal value to be used when the spawned
542+
process will be killed by timeout or abort signal. **Default:** `'SIGTERM'`.
533543

534544
* Returns: {ChildProcess}
535545

lib/child_process.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -651,15 +651,14 @@ function abortChildProcess(child, killSignal) {
651651
* @returns {ChildProcess}
652652
*/
653653
function spawn(file, args, options) {
654-
const child = new ChildProcess();
655654
options = normalizeSpawnArguments(file, args, options);
655+
validateTimeout(options.timeout, 'options.timeout');
656+
validateAbortSignal(options.signal, 'options.signal');
657+
const killSignal = sanitizeKillSignal(options.killSignal);
658+
const child = new ChildProcess();
656659

657660
if (options.signal) {
658661
const signal = options.signal;
659-
// Validate signal, if present
660-
validateAbortSignal(signal, 'options.signal');
661-
const killSignal = sanitizeKillSignal(options.killSignal);
662-
// Do nothing and throw if already aborted
663662
if (signal.aborted) {
664663
onAbortListener();
665664
} else {
@@ -678,6 +677,26 @@ function spawn(file, args, options) {
678677
debug('spawn', options);
679678
child.spawn(options);
680679

680+
if (options.timeout > 0) {
681+
let timeoutId = setTimeout(() => {
682+
if (timeoutId) {
683+
try {
684+
child.kill(killSignal);
685+
} catch (err) {
686+
child.emit('error', err);
687+
}
688+
timeoutId = null;
689+
}
690+
}, options.timeout);
691+
692+
child.once('exit', () => {
693+
if (timeoutId) {
694+
clearTimeout(timeoutId);
695+
timeoutId = null;
696+
}
697+
});
698+
}
699+
681700
return child;
682701
}
683702

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
const { mustCall } = require('../common');
5+
const { strictEqual, throws } = require('assert');
6+
const fixtures = require('../common/fixtures');
7+
const { fork } = require('child_process');
8+
const { getEventListeners } = require('events');
9+
const {
10+
EventTarget,
11+
} = require('internal/event_target');
12+
13+
{
14+
// Verify default signal
15+
const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), {
16+
timeout: 5,
17+
});
18+
cp.on('exit', mustCall((code, ks) => strictEqual(ks, 'SIGTERM')));
19+
}
20+
21+
{
22+
// Verify correct signal + closes after at least 4 ms.
23+
const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), {
24+
timeout: 5,
25+
killSignal: 'SIGKILL',
26+
});
27+
cp.on('exit', mustCall((code, ks) => strictEqual(ks, 'SIGKILL')));
28+
}
29+
30+
{
31+
// Verify timeout verification
32+
throws(() => fork(fixtures.path('child-process-stay-alive-forever.js'), {
33+
timeout: 'badValue',
34+
}), /ERR_OUT_OF_RANGE/);
35+
36+
throws(() => fork(fixtures.path('child-process-stay-alive-forever.js'), {
37+
timeout: {},
38+
}), /ERR_OUT_OF_RANGE/);
39+
}
40+
41+
{
42+
// Verify abort signal gets unregistered
43+
const signal = new EventTarget();
44+
signal.aborted = false;
45+
46+
const cp = fork(fixtures.path('child-process-stay-alive-forever.js'), {
47+
timeout: 6,
48+
signal,
49+
});
50+
strictEqual(getEventListeners(signal, 'abort').length, 1);
51+
cp.on('exit', mustCall(() => {
52+
strictEqual(getEventListeners(signal, 'abort').length, 0);
53+
}));
54+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Flags: --experimental-abortcontroller
2+
'use strict';
3+
4+
const { mustCall } = require('../common');
5+
const { strictEqual, throws } = require('assert');
6+
const fixtures = require('../common/fixtures');
7+
const { spawn } = require('child_process');
8+
const { getEventListeners } = require('events');
9+
10+
const aliveForeverFile = 'child-process-stay-alive-forever.js';
11+
{
12+
// Verify default signal + closes
13+
const cp = spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
14+
timeout: 5,
15+
});
16+
cp.on('exit', mustCall((code, ks) => strictEqual(ks, 'SIGTERM')));
17+
}
18+
19+
{
20+
// Verify SIGKILL signal + closes
21+
const cp = spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
22+
timeout: 6,
23+
killSignal: 'SIGKILL',
24+
});
25+
cp.on('exit', mustCall((code, ks) => strictEqual(ks, 'SIGKILL')));
26+
}
27+
28+
{
29+
// Verify timeout verification
30+
throws(() => spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
31+
timeout: 'badValue',
32+
}), /ERR_OUT_OF_RANGE/);
33+
34+
throws(() => spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
35+
timeout: {},
36+
}), /ERR_OUT_OF_RANGE/);
37+
}
38+
39+
{
40+
// Verify abort signal gets unregistered
41+
const controller = new AbortController();
42+
const { signal } = controller;
43+
const cp = spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
44+
timeout: 6,
45+
signal,
46+
});
47+
strictEqual(getEventListeners(signal, 'abort').length, 1);
48+
cp.on('exit', mustCall(() => {
49+
strictEqual(getEventListeners(signal, 'abort').length, 0);
50+
}));
51+
}

0 commit comments

Comments
 (0)