From 175c0ded3e4091d5addb91c43d57f562d7e8bbba Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 21 Jan 2024 22:12:43 +0000 Subject: [PATCH] Move `forceKillAfterTimeout` options to top-level --- index.d.ts | 47 ++++++++---------- index.js | 15 ++++-- index.test-d.ts | 13 +++-- lib/kill.js | 37 +++++++------- lib/stream.js | 17 +++---- readme.md | 55 +++++++++------------ test/fixtures/no-killable.js | 8 +-- test/kill.js | 96 +++++++++++++++++++++++++++--------- 8 files changed, 167 insertions(+), 121 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1c2d4deb7c..5f834ac7a2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -468,6 +468,26 @@ type CommonOptions = { */ readonly killSignal?: string | number; + /** + If the child process is terminated but does not exit, forcefully exit it by sending [`SIGKILL`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGKILL). + + The graceful period is 5 seconds by default. This feature can be disabled with `false`. + + This works when the child process is terminated by either: + - the `signal`, `timeout`, `maxBuffer` or `cleanup` option + - calling [`subprocess.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) with no arguments + + This does not work when the child process is terminated by either: + - calling [`subprocess.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) with an argument + - calling [`process.kill(subprocess.pid)`](https://nodejs.org/api/process.html#processkillpid-signal) + - sending a termination signal from another process + + Also, this does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events): `SIGKILL` and `SIGTERM` both terminate the process immediately. Other packages (such as [`taskkill`](https://github.com/sindresorhus/taskkill)) can be used to achieve graceful termination on Windows. + + @default 5000 + */ + forceKillAfterTimeout?: number | false; + /** If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`. @@ -721,17 +741,6 @@ type ExecaCommonError = { export type ExecaError = ExecaCommonReturnValue & ExecaCommonError; export type ExecaSyncError = ExecaCommonReturnValue & ExecaCommonError; -export type KillOptions = { - /** - Milliseconds to wait for the child process to terminate before sending `SIGKILL`. - - Can be disabled with `false`. - - @default 5000 - */ - forceKillAfterTimeout?: number | false; -}; - type StreamUnlessIgnored< StreamIndex extends string, OptionsType extends Options = Options, @@ -779,11 +788,6 @@ export type ExecaChildPromise = { onRejected?: (reason: ExecaError) => ResultType | PromiseLike ): Promise | ResultType>; - /** - Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill). - */ - kill(signal?: string, options?: KillOptions): void; - /** [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: - Another `execa()` return value @@ -917,17 +921,6 @@ try { \*\/ } ``` - -@example Graceful termination -``` -const subprocess = execa('node'); - -setTimeout(() => { - subprocess.kill('SIGTERM', { - forceKillAfterTimeout: 2000 - }); -}, 1000); -``` */ export function execa( file: string | URL, diff --git a/index.js b/index.js index b21513573e..2d48af824f 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,7 @@ import {makeError} from './lib/error.js'; import {handleInputAsync, pipeOutputAsync} from './lib/stdio/async.js'; import {handleInputSync, pipeOutputSync} from './lib/stdio/sync.js'; import {normalizeStdioNode} from './lib/stdio/normalize.js'; -import {spawnedKill, validateTimeout} from './lib/kill.js'; +import {spawnedKill, validateTimeout, normalizeForceKillAfterTimeout} from './lib/kill.js'; import {addPipeMethods} from './lib/pipe.js'; import {getSpawnedResult, makeAllStream} from './lib/stream.js'; import {mergePromise} from './lib/promise.js'; @@ -52,6 +52,7 @@ const handleArguments = (rawFile, rawArgs, rawOptions = {}) => { const options = addDefaultOptions(initialOptions); options.shell = normalizeFileUrl(options.shell); options.env = getEnv(options); + options.forceKillAfterTimeout = normalizeForceKillAfterTimeout(options.forceKillAfterTimeout); if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { // #116 @@ -79,6 +80,7 @@ const addDefaultOptions = ({ windowsHide = true, verbose = verboseDefault, killSignal = 'SIGTERM', + forceKillAfterTimeout = true, ...options }) => ({ ...options, @@ -97,6 +99,7 @@ const addDefaultOptions = ({ windowsHide, verbose, killSignal, + forceKillAfterTimeout, }); const handleOutput = (options, value) => { @@ -140,26 +143,28 @@ export function execa(rawFile, rawArgs, rawOptions) { return dummySpawned; } + const controller = new AbortController(); + pipeOutputAsync(spawned, stdioStreamsGroups); - spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); + spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned), options, controller); spawned.all = makeAllStream(spawned, options); addPipeMethods(spawned); - const promise = handlePromise({spawned, options, stdioStreamsGroups, command, escapedCommand}); + const promise = handlePromise({spawned, options, stdioStreamsGroups, command, escapedCommand, controller}); mergePromise(spawned, promise); return spawned; } -const handlePromise = async ({spawned, options, stdioStreamsGroups, command, escapedCommand}) => { +const handlePromise = async ({spawned, options, stdioStreamsGroups, command, escapedCommand, controller}) => { const context = {timedOut: false}; const [ [exitCode, signal, error], stdioResults, allResult, - ] = await getSpawnedResult(spawned, options, context, stdioStreamsGroups); + ] = await getSpawnedResult({spawned, options, context, stdioStreamsGroups, controller}); const stdio = stdioResults.map(stdioResult => handleOutput(options, stdioResult)); const all = handleOutput(options, allResult); diff --git a/index.test-d.ts b/index.test-d.ts index 4ee948c3fa..73c48bba72 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1187,6 +1187,14 @@ execa('unicorns', {killSignal: 'SIGTERM'}); execaSync('unicorns', {killSignal: 'SIGTERM'}); execa('unicorns', {killSignal: 9}); execaSync('unicorns', {killSignal: 9}); +execa('unicorns', {forceKillAfterTimeout: false}); +execaSync('unicorns', {forceKillAfterTimeout: false}); +execa('unicorns', {forceKillAfterTimeout: 42}); +execaSync('unicorns', {forceKillAfterTimeout: 42}); +execa('unicorns', {forceKillAfterTimeout: undefined}); +execaSync('unicorns', {forceKillAfterTimeout: undefined}); +expectError(execa('unicorns', {forceKillAfterTimeout: 'true'})); +expectError(execaSync('unicorns', {forceKillAfterTimeout: 'true'})); execa('unicorns', {signal: new AbortController().signal}); expectError(execaSync('unicorns', {signal: new AbortController().signal})); execa('unicorns', {windowsVerbatimArguments: true}); @@ -1199,10 +1207,7 @@ execaSync('unicorns', {verbose: false}); execa('unicorns').kill(); execa('unicorns').kill('SIGKILL'); execa('unicorns').kill(undefined); -execa('unicorns').kill('SIGKILL', {}); -execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: false}); -execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: 42}); -execa('unicorns').kill('SIGKILL', {forceKillAfterTimeout: undefined}); +expectError(execa('unicorns').kill('SIGKILL', {})); expectError(execa(['unicorns', 'arg'])); expectType(execa('unicorns')); diff --git a/lib/kill.js b/lib/kill.js index 7cc683634a..ae1067109d 100644 --- a/lib/kill.js +++ b/lib/kill.js @@ -1,34 +1,36 @@ import os from 'node:os'; -import {setTimeout as pSetTimeout} from 'node:timers/promises'; +import {setTimeout} from 'node:timers/promises'; import {onExit} from 'signal-exit'; const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior -export const spawnedKill = (kill, signal = 'SIGTERM', {forceKillAfterTimeout = true} = {}) => { +export const spawnedKill = (kill, {forceKillAfterTimeout}, controller, signal) => { const killResult = kill(signal); - const timeout = getForceKillAfterTimeout(signal, forceKillAfterTimeout, killResult); - setKillTimeout(kill, timeout); + setKillTimeout({kill, signal, forceKillAfterTimeout, killResult, controller}); return killResult; }; -const setKillTimeout = async (kill, timeout) => { - if (timeout === undefined) { +const setKillTimeout = async ({kill, signal, forceKillAfterTimeout, killResult, controller}) => { + if (!shouldForceKill(signal, forceKillAfterTimeout, killResult)) { return; } - await pSetTimeout(timeout, undefined, {ref: false}); - kill('SIGKILL'); + try { + await setTimeout(forceKillAfterTimeout, undefined, {signal: controller.signal}); + kill('SIGKILL'); + } catch {} }; const shouldForceKill = (signal, forceKillAfterTimeout, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; -const isSigterm = signal => signal === os.constants.signals.SIGTERM +const isSigterm = signal => signal === undefined + || signal === os.constants.signals.SIGTERM || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); -const getForceKillAfterTimeout = (signal, forceKillAfterTimeout, killResult) => { - if (!shouldForceKill(signal, forceKillAfterTimeout, killResult)) { - return; +export const normalizeForceKillAfterTimeout = forceKillAfterTimeout => { + if (forceKillAfterTimeout === false) { + return forceKillAfterTimeout; } if (forceKillAfterTimeout === true) { @@ -43,20 +45,18 @@ const getForceKillAfterTimeout = (signal, forceKillAfterTimeout, killResult) => }; const killAfterTimeout = async ({spawned, timeout, killSignal, context, controller}) => { - await pSetTimeout(timeout, undefined, {ref: false, signal: controller.signal}); + await setTimeout(timeout, undefined, {signal: controller.signal}); spawned.kill(killSignal); Object.assign(context, {timedOut: true, signal: killSignal}); throw new Error('Timed out'); }; // `timeout` option handling -export const throwOnTimeout = ({spawned, timeout, killSignal, context, finalizers}) => { +export const throwOnTimeout = ({spawned, timeout, killSignal, context, controller}) => { if (timeout === 0 || timeout === undefined) { return []; } - const controller = new AbortController(); - finalizers.push(controller.abort.bind(controller)); return [killAfterTimeout({spawned, timeout, killSignal, context, controller})]; }; @@ -67,13 +67,12 @@ export const validateTimeout = ({timeout}) => { }; // `cleanup` option handling -export const cleanupOnExit = (spawned, cleanup, detached, finalizers) => { +export const cleanupOnExit = (spawned, cleanup, detached) => { if (!cleanup || detached) { return; } - const removeExitHandler = onExit(() => { + return onExit(() => { spawned.kill(); }); - finalizers.push(removeExitHandler); }; diff --git a/lib/stream.js b/lib/stream.js index 3792b85bc9..167de020d5 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -110,14 +110,14 @@ const cleanupStdioStreams = (customStreams, error) => { }; // Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) -export const getSpawnedResult = async ( +export const getSpawnedResult = async ({ spawned, - {encoding, buffer, maxBuffer, timeout, killSignal, cleanup, detached}, + options: {encoding, buffer, maxBuffer, timeout, killSignal, cleanup, detached}, context, stdioStreamsGroups, -) => { - const finalizers = []; - cleanupOnExit(spawned, cleanup, detached, finalizers); + controller, +}) => { + const removeExitHandler = cleanupOnExit(spawned, cleanup, detached); const customStreams = getCustomStreams(stdioStreamsGroups); const stdioPromises = spawned.stdio.map((stream, index) => getStdioPromise({stream, stdioStreams: stdioStreamsGroups[index], encoding, buffer, maxBuffer})); @@ -133,7 +133,7 @@ export const getSpawnedResult = async ( ]), ...throwOnCustomStreamsError(customStreams), ...throwIfStreamError(spawned.stdin), - ...throwOnTimeout({spawned, timeout, killSignal, context, finalizers}), + ...throwOnTimeout({spawned, timeout, killSignal, context, controller}), ]); } catch (error) { spawned.kill(); @@ -145,8 +145,7 @@ export const getSpawnedResult = async ( cleanupStdioStreams(customStreams, error); return results; } finally { - for (const finalizer of finalizers) { - finalizer(); - } + controller.abort(); + removeExitHandler?.(); } }; diff --git a/readme.md b/readme.md index c590b88ac3..09f53d2135 100644 --- a/readme.md +++ b/readme.md @@ -50,7 +50,7 @@ This package improves [`child_process`](https://nodejs.org/api/child_process.htm - Redirect [`stdin`](#stdin)/[`stdout`](#stdout-1)/[`stderr`](#stderr-1) from/to files, streams, iterables, strings, `Uint8Array` or [objects](docs/transform.md#object-mode). - [Transform](docs/transform.md) `stdin`/`stdout`/`stderr` with simple functions. - Iterate over [each text line](docs/transform.md#binary-data) output by the process. -- [Graceful termination](#optionsforcekillaftertimeout). +- [Graceful termination](#forcekillaftertimeout). - Get [interleaved output](#all) from `stdout` and `stderr` similar to what is printed on the terminal. - [Strips the final newline](#stripfinalnewline) from the output so you don't have to do `stdout.trim()`. - Convenience methods to pipe processes' [input](#input) and [output](#redirect-output-to-a-file). @@ -221,20 +221,6 @@ try { } ``` -### Graceful termination - -Using SIGTERM, and after 2 seconds, kill it with SIGKILL. - -```js -const subprocess = execa('node'); - -setTimeout(() => { - subprocess.kill('SIGTERM', { - forceKillAfterTimeout: 2000 - }); -}, 1000); -``` - ## API ### Methods @@ -323,21 +309,6 @@ The return value of all [asynchronous methods](#methods) is both: - a `Promise` resolving or rejecting with a [`childProcessResult`](#childProcessResult). - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with the following additional methods and properties. -#### kill(signal?, options?) - -Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal) except: if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. - -Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill). - -##### options.forceKillAfterTimeout - -Type: `number | false`\ -Default: `5000` - -Milliseconds to wait for the child process to terminate before sending `SIGKILL`. - -Can be disabled with `false`. - #### all Type: `ReadableStream | undefined` @@ -468,7 +439,7 @@ Whether the process was canceled using the [`signal`](#signal-1) option. Type: `boolean` Whether the process was terminated using either: -- [`childProcess.kill()`](#killsignal-options). +- `childProcess.kill()`. - A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events). #### signal @@ -476,7 +447,7 @@ Whether the process was terminated using either: Type: `string | undefined` The name of the signal (like `SIGFPE`) that terminated the process using either: -- [`childProcess.kill()`](#killsignal-options). +- `childProcess.kill()`. - A signal sent by another process. This case is [not supported on Windows](https://nodejs.org/api/process.html#signal-events). If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. @@ -783,6 +754,26 @@ Default: `SIGTERM` Signal value to be used when the spawned process will be killed. +#### forceKillAfterTimeout + +Type: `number | false`\ +Default: `5000` + +If the child process is terminated but does not exit, forcefully exit it by sending [`SIGKILL`](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGKILL). + +The graceful period is 5 seconds by default. This feature can be disabled with `false`. + +This works when the child process is terminated by either: +- the [`signal`](#signal-1), [`timeout`](#timeout), [`maxBuffer`](#maxbuffer) or [`cleanup`](#cleanup) option +- calling [`subprocess.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) with no arguments + +This does not work when the child process is terminated by either: +- calling [`subprocess.kill()`](https://nodejs.org/api/child_process.html#subprocesskillsignal) with an argument +- calling [`process.kill(subprocess.pid)`](https://nodejs.org/api/process.html#processkillpid-signal) +- sending a termination signal from another process + +Also, this does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events): `SIGKILL` and `SIGTERM` both terminate the process immediately. Other packages (such as [`taskkill`](https://github.com/sindresorhus/taskkill)) can be used to achieve graceful termination on Windows. + #### signal Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) diff --git a/test/fixtures/no-killable.js b/test/fixtures/no-killable.js index b27edf71d3..05568a2e87 100755 --- a/test/fixtures/no-killable.js +++ b/test/fixtures/no-killable.js @@ -1,11 +1,13 @@ #!/usr/bin/env node import process from 'node:process'; -process.on('SIGTERM', () => { - console.log('Received SIGTERM, but we ignore it'); -}); +const noop = () => {}; + +process.on('SIGTERM', noop); +process.on('SIGINT', noop); process.send(''); +console.log('.'); setInterval(() => { // Run forever diff --git a/test/kill.js b/test/kill.js index 3a573536f6..63d1b767b4 100644 --- a/test/kill.js +++ b/test/kill.js @@ -10,58 +10,110 @@ setFixtureDir(); const TIMEOUT_REGEXP = /timed out after/; -test('kill("SIGKILL") should terminate cleanly', async t => { - const subprocess = execa('no-killable.js', {stdio: ['ipc']}); +const spawnNoKillable = async (forceKillAfterTimeout, options) => { + const subprocess = execa('no-killable.js', { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'], + forceKillAfterTimeout, + ...options, + }); await pEvent(subprocess, 'message'); + return {subprocess}; +}; + +test('kill("SIGKILL") should terminate cleanly', async t => { + const {subprocess} = await spawnNoKillable(); subprocess.kill('SIGKILL'); - const {signal} = await t.throwsAsync(subprocess); + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.true(isTerminated); t.is(signal, 'SIGKILL'); }); // `SIGTERM` cannot be caught on Windows, and it always aborts the process (like `SIGKILL` on Unix). // Therefore, this feature and those tests do not make sense on Windows. if (process.platform !== 'win32') { - test('`forceKillAfterTimeout: false` should not kill after a timeout', async t => { - const subprocess = execa('no-killable.js', {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); + const testNoForceKill = async (t, forceKillAfterTimeout, killArgument) => { + const {subprocess} = await spawnNoKillable(forceKillAfterTimeout); - subprocess.kill('SIGTERM', {forceKillAfterTimeout: false}); + subprocess.kill(killArgument); + await setTimeout(6e3); t.true(isRunning(subprocess.pid)); subprocess.kill('SIGKILL'); - const {signal} = await t.throwsAsync(subprocess); + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.true(isTerminated); t.is(signal, 'SIGKILL'); - }); + }; - const testForceKill = async (t, killArguments) => { - const subprocess = execa('no-killable.js', {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); + test('`forceKillAfterTimeout: false` should not kill after a timeout', testNoForceKill, false); + test('`forceKillAfterTimeout` should not kill after a timeout with other signals', testNoForceKill, true, 'SIGINT'); - subprocess.kill(...killArguments); + const testForceKill = async (t, forceKillAfterTimeout, killArgument) => { + const {subprocess} = await spawnNoKillable(forceKillAfterTimeout); - const {signal} = await t.throwsAsync(subprocess); + subprocess.kill(killArgument); + + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.true(isTerminated); t.is(signal, 'SIGKILL'); }; - test('`forceKillAfterTimeout: number` should kill after a timeout', testForceKill, ['SIGTERM', {forceKillAfterTimeout: 50}]); - test('`forceKillAfterTimeout: true` should kill after a timeout', testForceKill, ['SIGTERM', {forceKillAfterTimeout: true}]); - test('kill("SIGTERM") should kill after a timeout', testForceKill, ['SIGTERM']); - test('kill() with no arguments should kill after a timeout', testForceKill, []); + test('`forceKillAfterTimeout: number` should kill after a timeout', testForceKill, 50); + test('`forceKillAfterTimeout: true` should kill after a timeout', testForceKill, true); + test('`forceKillAfterTimeout: undefined` should kill after a timeout', testForceKill, undefined); + test('`forceKillAfterTimeout` should kill after a timeout with the killSignal', testForceKill, 50, 'SIGTERM'); const testInvalidForceKill = async (t, forceKillAfterTimeout) => { - const childProcess = execa('noop.js'); t.throws(() => { - childProcess.kill('SIGTERM', {forceKillAfterTimeout}); + execa('empty.js', {forceKillAfterTimeout}); }, {instanceOf: TypeError, message: /non-negative integer/}); - const {signal} = await t.throwsAsync(childProcess); - t.is(signal, 'SIGTERM'); }; test('`forceKillAfterTimeout` should not be NaN', testInvalidForceKill, Number.NaN); test('`forceKillAfterTimeout` should not be negative', testInvalidForceKill, -1); + + test('`forceKillAfterTimeout` works with the "signal" option', async t => { + const abortController = new AbortController(); + const {subprocess} = await spawnNoKillable(1, {signal: abortController.signal}); + abortController.abort(); + const {isTerminated, signal, isCanceled} = await t.throwsAsync(subprocess); + t.false(isTerminated); + t.is(signal, undefined); + t.true(isCanceled); + }); + + test.serial('`forceKillAfterTimeout` works with the "timeout" option', async t => { + const {subprocess} = await spawnNoKillable(1, {timeout: 2e3}); + const {isTerminated, signal, timedOut} = await t.throwsAsync(subprocess); + t.true(isTerminated); + t.is(signal, 'SIGTERM'); + t.true(timedOut); + }); + + test('`forceKillAfterTimeout` works with the "maxBuffer" option', async t => { + const {subprocess} = await spawnNoKillable(1, {maxBuffer: 1}); + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.false(isTerminated); + t.is(signal, undefined); + }); + + test('`forceKillAfterTimeout` works with "error" events on childProcess', async t => { + const {subprocess} = await spawnNoKillable(1); + subprocess.emit('error', new Error('test')); + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.false(isTerminated); + t.is(signal, undefined); + }); + + test('`forceKillAfterTimeout` works with "error" events on childProcess.stdout', async t => { + const {subprocess} = await spawnNoKillable(1); + subprocess.stdout.destroy(new Error('test')); + const {isTerminated, signal} = await t.throwsAsync(subprocess); + t.false(isTerminated); + t.is(signal, undefined); + }); } test('execa() returns a promise with kill()', async t => {