Skip to content

Commit

Permalink
child_process: add AbortSignal support
Browse files Browse the repository at this point in the history
PR-URL: #36308
Backport-PR-URL: #38386
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
benjamingr authored and targos committed Apr 30, 2021
1 parent 336fb18 commit a5c0f39
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 3 deletions.
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 @@ -45,15 +45,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 @@ -232,6 +241,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 @@ -349,6 +361,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
15 changes: 15 additions & 0 deletions test/parallel/test-child-process-execfile.js
@@ -1,3 +1,4 @@
// Flags: --experimental-abortcontroller
'use strict';

const common = require('../common');
Expand All @@ -7,6 +8,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 +47,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();
}

0 comments on commit a5c0f39

Please sign in to comment.