diff --git a/index.d.ts b/index.d.ts index bdd9c94161..2f846e6922 100644 --- a/index.d.ts +++ b/index.d.ts @@ -851,7 +851,8 @@ export type ExecaChildPromise = { [More info.](https://nodejs.org/api/child_process.html#subprocesskillsignal) */ - kill(signalOrError: Parameters[0] | Error): ReturnType; + kill(signal: Parameters[0], error?: Error): ReturnType; + kill(error?: Error): ReturnType; }; export type ExecaChildProcess = ChildProcess & diff --git a/index.test-d.ts b/index.test-d.ts index b850e3a7fe..ce110b8ac2 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1300,11 +1300,15 @@ expectType(execa('unicorns').kill()); execa('unicorns').kill('SIGKILL'); execa('unicorns').kill(undefined); execa('unicorns').kill(new Error('test')); -expectError(execa('unicorns').kill('SIGKILL', {})); +execa('unicorns').kill('SIGKILL', new Error('test')); +execa('unicorns').kill(undefined, new Error('test')); expectError(execa('unicorns').kill(null)); expectError(execa('unicorns').kill(0n)); expectError(execa('unicorns').kill([new Error('test')])); expectError(execa('unicorns').kill({message: 'test'})); +expectError(execa('unicorns').kill(undefined, {})); +expectError(execa('unicorns').kill('SIGKILL', {})); +expectError(execa('unicorns').kill(null, new Error('test'))); expectError(execa(['unicorns', 'arg'])); expectType(execa('unicorns')); diff --git a/lib/kill.js b/lib/kill.js index ca873a8457..f7e597cd3e 100644 --- a/lib/kill.js +++ b/lib/kill.js @@ -8,24 +8,34 @@ import {DiscardedError} from './error.js'; const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; // Monkey-patches `childProcess.kill()` to add `forceKillAfterDelay` behavior and `.kill(error)` -export const spawnedKill = ({kill, spawned, options: {forceKillAfterDelay, killSignal}, controller}, signalOrError = killSignal) => { - const signal = handleKillError(signalOrError, spawned, killSignal); +export const spawnedKill = ({kill, spawned, options: {forceKillAfterDelay, killSignal}, controller}, signalOrError, errorArgument) => { + const {signal, error} = parseKillArguments(signalOrError, errorArgument, killSignal); + emitKillError(spawned, error); const killResult = kill(signal); setKillTimeout({kill, signal, forceKillAfterDelay, killSignal, killResult, controller}); return killResult; }; -const handleKillError = (signalOrError, spawned, killSignal) => { - if (typeof signalOrError === 'string' || typeof signalOrError === 'number') { - return signalOrError; +const parseKillArguments = (signalOrError, errorArgument, killSignal) => { + const [signal = killSignal, error] = isErrorInstance(signalOrError) + ? [undefined, signalOrError] + : [signalOrError, errorArgument]; + + if (typeof signal !== 'string' && typeof signal !== 'number') { + throw new TypeError(`The first argument must be an error instance or a signal name string/number: ${signal}`); } - if (isErrorInstance(signalOrError)) { - spawned.emit(errorSignal, signalOrError); - return killSignal; + if (error !== undefined && !isErrorInstance(error)) { + throw new TypeError(`The second argument is optional. If specified, it must be an error instance: ${error}`); } - throw new TypeError(`The first argument must be an error instance or a signal name string/number: ${signalOrError}`); + return {signal, error}; +}; + +const emitKillError = (spawned, error) => { + if (error !== undefined) { + spawned.emit(errorSignal, error); + } }; // Like `error` signal but internal to Execa. diff --git a/readme.md b/readme.md index d2670b2605..66b522fd98 100644 --- a/readme.md +++ b/readme.md @@ -325,9 +325,11 @@ A `streamName` can be passed to pipe `"stderr"`, `"all"` (both `stdout` and `std Returns `execaChildProcess`, which allows chaining `.pipe()` then `await`ing the [final result](#childprocessresult). -#### kill(signalOrError?) +#### kill(signal, error?) +#### kill(error?) -`signalOrError`: `string | number | Error`\ +`signal`: `string | number`\ +`error`: `Error`\ _Returns_: `boolean` Sends a [signal](https://nodejs.org/api/os.html#signal-constants) to the child process. The default signal is the [`killSignal`](#killsignal) option. `killSignal` defaults to `SIGTERM`, which [terminates](#isterminated) the child process. diff --git a/test/kill.js b/test/kill.js index a290f1a9de..421eea4d47 100644 --- a/test/kill.js +++ b/test/kill.js @@ -427,11 +427,14 @@ test('child process errors use killSignal', async t => { t.is(thrownError.signal, 'SIGINT'); }); -const testInvalidKillArgument = async (t, killArgument) => { +const testInvalidKillArgument = async (t, killArgument, secondKillArgument) => { const subprocess = execa('empty.js'); + const message = secondKillArgument instanceof Error || secondKillArgument === undefined + ? /error instance or a signal name/ + : /second argument is optional/; t.throws(() => { - subprocess.kill(killArgument); - }, {message: /error instance or a signal name/}); + subprocess.kill(killArgument, secondKillArgument); + }, {message}); await subprocess; }; @@ -440,6 +443,9 @@ test('Cannot call .kill(0n)', testInvalidKillArgument, 0n); test('Cannot call .kill(true)', testInvalidKillArgument, true); test('Cannot call .kill(errorObject)', testInvalidKillArgument, {name: '', message: '', stack: ''}); test('Cannot call .kill([error])', testInvalidKillArgument, [new Error('test')]); +test('Cannot call .kill(undefined, true)', testInvalidKillArgument, undefined, true); +test('Cannot call .kill("SIGTERM", true)', testInvalidKillArgument, 'SIGTERM', true); +test('Cannot call .kill(true, error)', testInvalidKillArgument, true, new Error('test')); test('.kill(error) propagates error', async t => { const subprocess = execa('forever.js'); @@ -458,8 +464,18 @@ test('.kill(error) propagates error', async t => { test('.kill(error) uses killSignal', async t => { const subprocess = execa('forever.js', {killSignal: 'SIGINT'}); - subprocess.kill(new Error('test')); - t.like(await t.throwsAsync(subprocess), {signal: 'SIGINT'}); + const error = new Error('test'); + subprocess.kill(error); + t.is(await t.throwsAsync(subprocess), error); + t.is(error.signal, 'SIGINT'); +}); + +test('.kill(signal, error) uses signal', async t => { + const subprocess = execa('forever.js'); + const error = new Error('test'); + subprocess.kill('SIGINT', error); + t.is(await t.throwsAsync(subprocess), error); + t.is(error.signal, 'SIGINT'); }); test('.kill(error) is a noop if process already exited', async t => {