From 8f3cf59f83db21d3540da0a9cf53a1f4b44f1f83 Mon Sep 17 00:00:00 2001 From: Benjamin Gruenbaum <benjamingr@gmail.com> Date: Mon, 7 Dec 2020 19:02:06 +0200 Subject: [PATCH] child_process: add signal support to spawn --- doc/api/child_process.md | 23 ++++++++++++++-- lib/child_process.js | 26 ++++++++++++++++++- .../test-child-process-spawn-controller.js | 21 +++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-child-process-spawn-controller.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 1972d6c39bb1dd..44962a7840ae9d 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -280,7 +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 + * `signal` {AbortSignal} allows aborting the execFile using an AbortSignal. * `callback` {Function} Called with the output when process terminates. * `error` {Error} * `stdout` {string|Buffer} @@ -344,7 +344,7 @@ const { signal } = controller; const child = execFile('node', ['--version'], { signal }, (error) => { console.log(error); // an AbortError }); -signal.abort(); +controller.abort(); ``` ### `child_process.fork(modulePath[, args][, options])` @@ -424,6 +424,9 @@ The `shell` option available in [`child_process.spawn()`][] is not supported by <!-- YAML added: v0.1.90 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/36432 + description: AbortSignal support was added. - version: - v13.2.0 - v12.16.0 @@ -466,6 +469,8 @@ changes: when `shell` is specified and is CMD. **Default:** `false`. * `windowsHide` {boolean} Hide the subprocess console window that would normally be created on Windows systems. **Default:** `false`. + * `signal` {AbortSignal} allows aborting the execFile using an AbortSignal. + * Returns: {ChildProcess} The `child_process.spawn()` method spawns a new process using the given @@ -572,6 +577,20 @@ Node.js currently overwrites `argv[0]` with `process.execPath` on startup, so parameter passed to `spawn` from the parent, retrieve it with the `process.argv0` property instead. +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 grep = spawn('grep', ['ssh'], { signal }); +grep.on('error', (err) => { + // This will be called with err being an AbortError if the controller aborts +}); +controller.abort(); // stops the process +``` + #### `options.detached` <!-- YAML added: v0.7.10 diff --git a/lib/child_process.js b/lib/child_process.js index af49655ec75cfe..ae5a5f6587d1af 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -749,6 +749,30 @@ function sanitizeKillSignal(killSignal) { } } +// This level of indirection is here because the other child_process methods +// call spawn internally but should use different cancellation logic. +function spawnWithSignal(file, args, options) { + const child = spawn(file, args, options); + + if (options && options.signal) { + // Validate signal, if present + validateAbortSignal(options.signal, 'options.signal'); + function kill() { + if (child._handle) { + child._handle.kill('SIGTERM'); + child.emit('error', new AbortError()); + } + } + if (options.signal.aborted) { + process.nextTick(kill); + } else { + options.signal.addEventListener('abort', kill); + const remove = () => options.signal.removeEventListener('abort', kill); + child.once('close', remove); + } + } + return child; +} module.exports = { _forkChild, ChildProcess, @@ -757,6 +781,6 @@ module.exports = { execFileSync, execSync, fork, - spawn, + spawn: spawnWithSignal, spawnSync }; diff --git a/test/parallel/test-child-process-spawn-controller.js b/test/parallel/test-child-process-spawn-controller.js new file mode 100644 index 00000000000000..399558569cc1ec --- /dev/null +++ b/test/parallel/test-child-process-spawn-controller.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); + +// Verify that passing an AbortSignal works +const controller = new AbortController(); +const { signal } = controller; + +const echo = cp.spawn('echo', ['fun'], { + encoding: 'utf8', + shell: true, + signal +}); + +echo.on('error', common.mustCall((e) => { + assert.strictEqual(e.name, 'AbortError'); +})); + +controller.abort();