Skip to content

Commit 95aa032

Browse files
Nitzan Uzielyruyadorno
Nitzan Uziely
authored andcommitted
child_process: add timeout to spawn and fork
Add support for timeout to spawn and fork. Fixes: #27639 PR-URL: #37256 Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent d1a3e0e commit 95aa032

4 files changed

+138
-9
lines changed

doc/api/child_process.md

+14-4
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: v15.11.0
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: v15.11.0
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

+24-5
Original file line numberDiff line numberDiff line change
@@ -599,15 +599,14 @@ function abortChildProcess(child, killSignal) {
599599

600600

601601
function spawn(file, args, options) {
602-
const child = new ChildProcess();
603602
options = normalizeSpawnArguments(file, args, options);
603+
validateTimeout(options.timeout, 'options.timeout');
604+
validateAbortSignal(options.signal, 'options.signal');
605+
const killSignal = sanitizeKillSignal(options.killSignal);
606+
const child = new ChildProcess();
604607

605608
if (options.signal) {
606609
const signal = options.signal;
607-
// Validate signal, if present
608-
validateAbortSignal(signal, 'options.signal');
609-
const killSignal = sanitizeKillSignal(options.killSignal);
610-
// Do nothing and throw if already aborted
611610
if (signal.aborted) {
612611
onAbortListener();
613612
} else {
@@ -626,6 +625,26 @@ function spawn(file, args, options) {
626625
debug('spawn', options);
627626
child.spawn(options);
628627

628+
if (options.timeout > 0) {
629+
let timeoutId = setTimeout(() => {
630+
if (timeoutId) {
631+
try {
632+
child.kill(killSignal);
633+
} catch (err) {
634+
child.emit('error', err);
635+
}
636+
timeoutId = null;
637+
}
638+
}, options.timeout);
639+
640+
child.once('exit', () => {
641+
if (timeoutId) {
642+
clearTimeout(timeoutId);
643+
timeoutId = null;
644+
}
645+
});
646+
}
647+
629648
return child;
630649
}
631650

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

0 commit comments

Comments
 (0)