Skip to content

Commit

Permalink
Add .kill(signal, error) (#836)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored Feb 21, 2024
1 parent f5283d5 commit 30a662d
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 18 deletions.
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,8 @@ export type ExecaChildPromise<OptionsType extends Options = Options> = {
[More info.](https://nodejs.org/api/child_process.html#subprocesskillsignal)
*/
kill(signalOrError: Parameters<ChildProcess['kill']>[0] | Error): ReturnType<ChildProcess['kill']>;
kill(signal: Parameters<ChildProcess['kill']>[0], error?: Error): ReturnType<ChildProcess['kill']>;
kill(error?: Error): ReturnType<ChildProcess['kill']>;
};

export type ExecaChildProcess<OptionsType extends Options = Options> = ChildProcess &
Expand Down
6 changes: 5 additions & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1300,11 +1300,15 @@ expectType<boolean>(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<ExecaChildProcess>(execa('unicorns'));
Expand Down
28 changes: 19 additions & 9 deletions lib/kill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 21 additions & 5 deletions test/kill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -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');
Expand All @@ -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 => {
Expand Down

0 comments on commit 30a662d

Please sign in to comment.