Skip to content

Commit b68b13a

Browse files
Nitzan Uzielytargos
Nitzan Uziely
authored andcommittedSep 1, 2021
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 <benjamingr@gmail.com>
1 parent a7833c7 commit b68b13a

4 files changed

+143
-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: 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

+24-5
Original file line numberDiff line numberDiff line change
@@ -646,15 +646,14 @@ function abortChildProcess(child, killSignal) {
646646
* @returns {ChildProcess}
647647
*/
648648
function spawn(file, args, options) {
649-
const child = new ChildProcess();
650649
options = normalizeSpawnArguments(file, args, options);
650+
validateTimeout(options.timeout, 'options.timeout');
651+
validateAbortSignal(options.signal, 'options.signal');
652+
const killSignal = sanitizeKillSignal(options.killSignal);
653+
const child = new ChildProcess();
651654

652655
if (options.signal) {
653656
const signal = options.signal;
654-
// Validate signal, if present
655-
validateAbortSignal(signal, 'options.signal');
656-
const killSignal = sanitizeKillSignal(options.killSignal);
657-
// Do nothing and throw if already aborted
658657
if (signal.aborted) {
659658
onAbortListener();
660659
} else {
@@ -673,6 +672,26 @@ function spawn(file, args, options) {
673672
debug('spawn', options);
674673
child.spawn(options);
675674

675+
if (options.timeout > 0) {
676+
let timeoutId = setTimeout(() => {
677+
if (timeoutId) {
678+
try {
679+
child.kill(killSignal);
680+
} catch (err) {
681+
child.emit('error', err);
682+
}
683+
timeoutId = null;
684+
}
685+
}, options.timeout);
686+
687+
child.once('exit', () => {
688+
if (timeoutId) {
689+
clearTimeout(timeoutId);
690+
timeoutId = null;
691+
}
692+
});
693+
}
694+
676695
return child;
677696
}
678697

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+
}
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)
Please sign in to comment.