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();