From c6e791a47827fab6bbadbdb5d0328f7d542e099a Mon Sep 17 00:00:00 2001 From: Gyubong Date: Sun, 13 Feb 2022 19:27:01 +0900 Subject: [PATCH] Support `AbortController` (#490) Co-authored-by: Sindre Sorhus --- index.d.ts | 30 ++++++++++++++++++++++++++++++ index.js | 2 +- index.test-d.ts | 1 + readme.md | 21 +++++++++++++++------ test/kill.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index bcf04f922..f9cf41878 100644 --- a/index.d.ts +++ b/index.d.ts @@ -208,6 +208,34 @@ export interface CommonOptions { */ readonly killSignal?: string | number; + /** + You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + + When `AbortController.abort()` is called, [`.isCanceled`](https://github.com/sindresorhus/execa#iscanceled) becomes `false`. + + *Requires Node.js 16 or later.* + + @example + ```js + import {execa} from 'execa'; + + const abortController = new AbortController(); + const subprocess = execa('node', [], {signal: abortController.signal}); + + setTimeout(() => { + abortController.abort(); + }, 1000); + + try { + await subprocess; + } catch (error) { + console.log(subprocess.killed); // true + console.log(error.isCanceled); // true + } + ``` + */ + readonly signal?: AbortSignal; + /** 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`. @@ -341,6 +369,8 @@ export interface ExecaReturnValue /** Whether the process was canceled. + + You can cancel the spawned process using the [`signal`](https://github.com/sindresorhus/execa#signal-1) option. */ isCanceled: boolean; } diff --git a/index.js b/index.js index 9df491ded..f060590c8 100644 --- a/index.js +++ b/index.js @@ -127,7 +127,7 @@ export function execa(file, args, options) { escapedCommand, parsed, timedOut, - isCanceled: context.isCanceled, + isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), killed: spawned.killed, }); diff --git a/index.test-d.ts b/index.test-d.ts index e8d0a0d11..b73cfa036 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -142,6 +142,7 @@ execa('unicorns', {timeout: 1000}); execa('unicorns', {maxBuffer: 1000}); execa('unicorns', {killSignal: 'SIGTERM'}); execa('unicorns', {killSignal: 9}); +execa('unicorns', {signal: new AbortController().signal}); execa('unicorns', {windowsVerbatimArguments: true}); execa('unicorns', {windowsHide: false}); /* eslint-enable @typescript-eslint/no-floating-promises */ diff --git a/readme.md b/readme.md index c1aa8ea30..a529f3903 100644 --- a/readme.md +++ b/readme.md @@ -83,10 +83,11 @@ try { ```js import {execa} from 'execa'; -const subprocess = execa('node'); +const abortController = new AbortController(); +const subprocess = execa('node', [], {signal: abortController.signal}); setTimeout(() => { - subprocess.cancel(); + abortController.abort(); }, 1000); try { @@ -171,10 +172,6 @@ Milliseconds to wait for the child process to terminate before sending `SIGKILL` Can be disabled with `false`. -#### cancel() - -Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This is preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. - #### all Type: `ReadableStream | undefined` @@ -290,6 +287,8 @@ Type: `boolean` Whether the process was canceled. +You can cancel the spawned process using the [`signal`](#signal-1) option. + #### killed Type: `boolean` @@ -546,6 +545,16 @@ Default: `SIGTERM` Signal value to be used when the spawned process will be killed. +#### signal + +Type: [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) + +You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + +When `AbortController.abort()` is called, [`.isCanceled`](#iscanceled) becomes `false`. + +*Requires Node.js 16 or later.* + #### windowsVerbatimArguments Type: `boolean`\ diff --git a/test/kill.js b/test/kill.js index 08ae393bf..c26c58d49 100644 --- a/test/kill.js +++ b/test/kill.js @@ -262,3 +262,50 @@ test('calling cancel method on a process which has been killed does not make err const {isCanceled} = await t.throwsAsync(subprocess); t.false(isCanceled); }); + +if (globalThis.AbortController !== undefined) { + test('calling abort throws an error with message "Command was canceled"', async t => { + const abortController = new AbortController(); + const subprocess = execa('noop.js', [], {signal: abortController.signal}); + abortController.abort(); + await t.throwsAsync(subprocess, {message: /Command was canceled/}); + }); + + test('calling abort twice should show the same behaviour as calling it once', async t => { + const abortController = new AbortController(); + const subprocess = execa('noop.js', [], {signal: abortController.signal}); + abortController.abort(); + abortController.abort(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.true(isCanceled); + t.true(subprocess.killed); + }); + + test('calling abort on a successfully completed process does not make result.isCanceled true', async t => { + const abortController = new AbortController(); + const subprocess = execa('noop.js', [], {signal: abortController.signal}); + const {isCanceled} = await subprocess; + abortController.abort(); + t.false(isCanceled); + }); + + test('calling cancel after abort should show the same behaviour as only calling cancel', async t => { + const abortController = new AbortController(); + const subprocess = execa('noop.js', [], {signal: abortController.signal}); + abortController.abort(); + subprocess.cancel(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.true(isCanceled); + t.true(subprocess.killed); + }); + + test('calling abort after cancel should show the same behaviour as only calling cancel', async t => { + const abortController = new AbortController(); + const subprocess = execa('noop.js', [], {signal: abortController.signal}); + subprocess.cancel(); + abortController.abort(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.true(isCanceled); + t.true(subprocess.killed); + }); +}