Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

child_process: add AbortSignal support #36308

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/api/child_process.md
Expand Up @@ -250,6 +250,9 @@ lsExample();
<!-- YAML
added: v0.1.91
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/36308
description: AbortSignal support was added.
- version: v8.8.0
pr-url: https://github.com/nodejs/node/pull/15380
description: The `windowsHide` option is supported now.
Expand Down Expand Up @@ -277,6 +280,7 @@ changes:
`'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different
shell can be specified as a string. See [Shell requirements][] and
[Default Windows shell][]. **Default:** `false` (no shell).
* `signal` {AbortSignal} allows aborting the execFile using an AbortSignal
* `callback` {Function} Called with the output when process terminates.
* `error` {Error}
* `stdout` {string|Buffer}
Expand Down Expand Up @@ -330,6 +334,19 @@ getVersion();
function. Any input containing shell metacharacters may be used to trigger
arbitrary command execution.**

If the `signal` option is enabled, calling `.abort()` on the corresponding
`AbortController` is similar to calling `.kill()` on the child process except
the error passed to the callback will be an `AbortError`:

```js
const controller = new AbortController();
const { signal } = controller;
const child = execFile('node', ['--version'], { signal }, (error) => {
console.log(error); // an AbortError
});
signal.abort();
```

### `child_process.fork(modulePath[, args][, options])`
<!-- YAML
added: v0.5.0
Expand Down
32 changes: 29 additions & 3 deletions lib/child_process.js
Expand Up @@ -58,15 +58,24 @@ let debug = require('internal/util/debuglog').debuglog(
);
const { Buffer } = require('buffer');
const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap');

const {
AbortError,
codes: errorCodes,
} = require('internal/errors');
const {
ERR_INVALID_ARG_VALUE,
ERR_CHILD_PROCESS_IPC_REQUIRED,
ERR_CHILD_PROCESS_STDIO_MAXBUFFER,
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
ERR_OUT_OF_RANGE,
} = errorCodes;
const { clearTimeout, setTimeout } = require('timers');
const { validateString, isInt32 } = require('internal/validators');
const {
validateString,
isInt32,
validateAbortSignal,
} = require('internal/validators');
const child_process = require('internal/child_process');
const {
getValidStdio,
Expand Down Expand Up @@ -245,6 +254,9 @@ function execFile(file /* , args, options, callback */) {
// Validate maxBuffer, if present.
validateMaxBuffer(options.maxBuffer);

// Validate signal, if present
validateAbortSignal(options.signal, 'options.signal');

options.killSignal = sanitizeKillSignal(options.killSignal);

const child = spawn(file, args, {
Expand Down Expand Up @@ -362,6 +374,20 @@ function execFile(file /* , args, options, callback */) {
timeoutId = null;
}, options.timeout);
}
if (options.signal) {
if (options.signal.aborted) {
process.nextTick(() => kill());
} else {
options.signal.addEventListener('abort', () => {
if (!ex) {
ex = new AbortError();
}
kill();
});
const remove = () => options.signal.removeEventListener('abort', kill);
child.once('close', remove);
}
}

if (child.stdout) {
if (encoding)
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-bootstrap-modules.js
Expand Up @@ -114,6 +114,7 @@ if (!common.isMainThread) {
'Internal Binding performance',
'Internal Binding symbols',
'Internal Binding worker',
'NativeModule internal/streams/add-abort-signal',
'NativeModule internal/streams/duplex',
'NativeModule internal/streams/passthrough',
'NativeModule internal/streams/readable',
Expand Down
14 changes: 14 additions & 0 deletions test/parallel/test-child-process-execfile.js
Expand Up @@ -7,6 +7,7 @@ const { getSystemErrorName } = require('util');
const fixtures = require('../common/fixtures');

const fixture = fixtures.path('exit.js');
const echoFixture = fixtures.path('echo.js');
const execOpts = { encoding: 'utf8', shell: true };

{
Expand Down Expand Up @@ -45,3 +46,16 @@ const execOpts = { encoding: 'utf8', shell: true };
// Verify the shell option works properly
execFile(process.execPath, [fixture, 0], execOpts, common.mustSucceed());
}

{
// Verify that the signal option works properly
const ac = new AbortController();
const { signal } = ac;

const callback = common.mustCall((err) => {
assert.strictEqual(err.code, 'ABORT_ERR');
assert.strictEqual(err.name, 'AbortError');
});
execFile(process.execPath, [echoFixture, 0], { signal }, callback);
ac.abort();
}