Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ If a `callback` function is provided, it is called with the arguments
`error` will be an instance of [`Error`][]. The `error.code` property will be
the exit code of the process. By convention, any exit code other than `0`
indicates an error. `error.signal` will be the signal that terminated the
process.
process. `error.timedOut` will be `true` if the process was killed because
the `timeout` option expired.

The `stdout` and `stderr` arguments passed to the callback will contain the
stdout and stderr output of the child process. By default, Node.js will decode
Expand Down Expand Up @@ -2314,6 +2315,32 @@ subprocess.stdout.on('data', (data) => {
The `subprocess.stdout` property can be `null` or `undefined`
if the child process could not be successfully spawned.

### `subprocess.timedOut`

<!-- YAML
added: REPLACEME
-->

* Type: {boolean} Set to `true` when the child process is killed because the
`timeout` option expired.

The `subprocess.timedOut` property indicates whether the `timeout` option of
[`child_process.spawn()`][], [`child_process.exec()`][],
[`child_process.execFile()`][] or [`child_process.fork()`][] expired and the
child process was sent the `killSignal` as a result. Like
[`subprocess.killed`][], it does not indicate that the child process has
terminated yet.

```js
const { spawn } = require('node:child_process');

const subprocess = spawn('sleep', ['10'], { timeout: 100 });

subprocess.on('exit', (code, signal) => {
console.log(subprocess.timedOut); // true
});
```

### `subprocess.unref()`

<!-- YAML
Expand Down Expand Up @@ -2429,6 +2456,7 @@ or [`child_process.fork()`][].
[`subprocess.disconnect()`]: #subprocessdisconnect
[`subprocess.exitCode`]: #subprocessexitcode
[`subprocess.kill()`]: #subprocesskillsignal
[`subprocess.killed`]: #subprocesskilled
[`subprocess.send()`]: #subprocesssendmessage-sendhandle-options-callback
[`subprocess.signalCode`]: #subprocesssignalcode
[`subprocess.stderr`]: #subprocessstderr
Expand Down
12 changes: 9 additions & 3 deletions lib/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ function execFile(file, args, options, callback) {
});

ex.cmd = cmd;
ex.timedOut = child.timedOut;
callback(ex, stdout, stderr);
}

Expand All @@ -450,16 +451,19 @@ function execFile(file, args, options, callback) {

killed = true;
try {
child.kill(options.killSignal);
return child.kill(options.killSignal);
} catch (e) {
ex = e;
exithandler();
}
return false;
}

if (options.timeout > 0) {
timeoutId = setTimeout(function delayedKill() {
kill();
// The child may have exited already, with a descendant keeping its
// stdio open: kill() reports whether the signal was actually sent.
child.timedOut = kill();
timeoutId = null;
}, options.timeout);
}
Expand Down Expand Up @@ -895,7 +899,9 @@ function spawn(file, args, options) {
let timeoutId = setTimeout(() => {
if (timeoutId) {
try {
child.kill(killSignal);
// The child may have exited already, with a descendant keeping its
// stdio open: kill() reports whether the signal was actually sent.
child.timedOut = child.kill(killSignal);
} catch (err) {
child.emit('error', err);
}
Expand Down
1 change: 1 addition & 0 deletions lib/internal/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ function ChildProcess() {
this.signalCode = null;
this.exitCode = null;
this.killed = false;
this.timedOut = false;
this.spawnfile = null;

this._handle = new Process();
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-child-process-exec-timeout-expire.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ cp.exec(cmd, {

let sigterm = 'SIGTERM';
assert.strictEqual(err.killed, true);
assert.strictEqual(err.timedOut, true);
// TODO OpenBSD returns a null signal and 143 for code
if (common.isOpenBSD) {
assert.strictEqual(err.code, 143);
Expand Down
4 changes: 3 additions & 1 deletion test/parallel/test-child-process-exec-timeout-kill.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ if (process.argv[2] === 'child') {
const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`;

// Test with a different kill signal.
cp.exec(cmd, {
const child = cp.exec(cmd, {
...opts,
timeout: kExpiringParentTimer,
killSignal: 'SIGKILL'
Expand All @@ -30,6 +30,8 @@ cp.exec(cmd, {
console.log('[stderr]', stderr.trim());

assert.strictEqual(err.killed, true);
assert.strictEqual(err.timedOut, true);
assert.strictEqual(child.timedOut, true);
assert.strictEqual(err.code, null);
assert.strictEqual(err.signal, 'SIGKILL');
assert.strictEqual(err.cmd, cmd);
Expand Down
23 changes: 22 additions & 1 deletion test/parallel/test-child-process-exec-timeout-not-expired.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,35 @@ if (process.argv[2] === 'child') {
return;
}

// The property is also present on errors that are not caused by the timeout.
cp.execFile('this-command-does-not-exist', { timeout: 2 ** 30 },
common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
assert.strictEqual(err.timedOut, false);
}));

// The timeout expires after the child exited, while a descendant still keeps
// its stdio open: the signal is not sent, so timedOut stays false.
{
const child = cp.execFile(process.execPath, ['-e', `
const { spawn } = require('child_process');
spawn(process.execPath, ['-e', 'setTimeout(() => {}, ${common.platformTimeout(1500)})'],
{ stdio: 'inherit' }).unref();
`], { timeout: common.platformTimeout(500) }, common.mustSucceed(() => {
assert.strictEqual(child.exitCode, 0);
assert.strictEqual(child.timedOut, false);
}));
}

const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" "${__filename}" child`;

cp.exec(cmd, {
const child = cp.exec(cmd, {
...opts,
timeout: kTimeoutNotSupposedToExpire,
}, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'child stdout');
assert.strictEqual(stderr.trim(), 'child stderr');
assert.strictEqual(child.timedOut, false);
}));

cleanupStaleProcess(__filename);
45 changes: 43 additions & 2 deletions test/parallel/test-child-process-spawn-timeout-kill-signal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const { mustCall } = require('../common');
const common = require('../common');
const { mustCall } = common;
const assert = require('assert');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
Expand All @@ -12,7 +13,47 @@ const aliveForeverFile = 'child-process-stay-alive-forever.js';
const cp = spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
timeout: 5,
});
cp.on('exit', mustCall((code, ks) => assert.strictEqual(ks, 'SIGTERM')));
assert.strictEqual(cp.timedOut, false);
cp.on('exit', mustCall((code, ks) => {
assert.strictEqual(ks, 'SIGTERM');
assert.strictEqual(cp.timedOut, true);
}));
}

{
// Verify timedOut stays false when the child exits before the timeout
const cp = spawn(process.execPath, ['-e', ''], { timeout: 2 ** 30 });
cp.on('exit', mustCall((code) => {
assert.strictEqual(code, 0);
assert.strictEqual(cp.timedOut, false);
}));
}

{
// Verify timedOut stays false when the child exits in time but a descendant
// keeps its stdio open past the timeout, so that the timer fires after exit.
const cp = spawn(process.execPath, ['-e', `
const { spawn } = require('child_process');
spawn(process.execPath, ['-e', 'setTimeout(() => {}, ${common.platformTimeout(1500)})'],
{ stdio: 'inherit' }).unref();
`], { timeout: common.platformTimeout(500) });
cp.on('exit', mustCall((code) => {
assert.strictEqual(code, 0);
assert.strictEqual(cp.timedOut, false);
}));
cp.on('close', mustCall(() => assert.strictEqual(cp.timedOut, false)));
}

{
// Verify timedOut stays false when the child is killed by the caller
const cp = spawn(process.execPath, [fixtures.path(aliveForeverFile)], {
timeout: 2 ** 30,
});
cp.on('spawn', mustCall(() => cp.kill()));
cp.on('exit', mustCall(() => {
assert.strictEqual(cp.killed, true);
assert.strictEqual(cp.timedOut, false);
}));
}

{
Expand Down
Loading