From 93b8063257bfb79477a3505eb117f5b790a6c666 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 28 Apr 2019 13:19:56 +0200 Subject: [PATCH 01/51] Add feature to retry process killing after specified duration Implements #105 --- index.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/index.js b/index.js index bb3d217c2f..f3cdc86502 100644 --- a/index.js +++ b/index.js @@ -221,6 +221,28 @@ const execa = (file, args, options) => { return Promise.reject(error); } + const originalKill = spawned.kill.bind(spawned); + spawned.kill = (signal, options) => { + if (signal === undefined || signal === 'SIGTERM') { + const retryAfter = options !== undefined && + ['number', 'string'].includes(typeof options.retryAfter) + ? Number(options.retryAfter) + : 2000; + + setTimeout(() => { + try { + originalKill(0); + originalKill('SIGKILL'); + } catch (err) { + // All good - process killed + } + }, retryAfter); + originalKill(signal); + } else { + originalKill(signal); + } + }; + // #115 let removeExitHandler; if (parsed.options.cleanup && !parsed.options.detached) { From 9b8efe917c3db8f3480dc0ee41b5412c10139a7e Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 28 Apr 2019 13:31:02 +0200 Subject: [PATCH 02/51] Fixup: repair code according to styleguide --- index.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index f3cdc86502..0e4f49b600 100644 --- a/index.js +++ b/index.js @@ -225,19 +225,19 @@ const execa = (file, args, options) => { spawned.kill = (signal, options) => { if (signal === undefined || signal === 'SIGTERM') { const retryAfter = options !== undefined && - ['number', 'string'].includes(typeof options.retryAfter) - ? Number(options.retryAfter) - : 2000; - - setTimeout(() => { - try { - originalKill(0); - originalKill('SIGKILL'); - } catch (err) { - // All good - process killed - } - }, retryAfter); - originalKill(signal); + ['number', 'string'].includes(typeof options.retryAfter) ? + Number(options.retryAfter) : + 2000; + + setTimeout(() => { + try { + originalKill(0); + originalKill('SIGKILL'); + } catch (_) { + // All good - process killed + } + }, retryAfter); + originalKill(signal); } else { originalKill(signal); } From fcdcaba902403cf77aab6834c882b53c4d02dff2 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 28 Apr 2019 13:45:11 +0200 Subject: [PATCH 03/51] Fixup: Return kill result from wrapper --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 0e4f49b600..a3a75e4bbb 100644 --- a/index.js +++ b/index.js @@ -237,10 +237,10 @@ const execa = (file, args, options) => { // All good - process killed } }, retryAfter); - originalKill(signal); - } else { - originalKill(signal); + return originalKill(signal); } + + return originalKill(signal); }; // #115 From 3ea8c34b3db87facc55377a672f1103b80522e74 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 29 Apr 2019 21:08:04 +0200 Subject: [PATCH 04/51] Fixup: Don't kill without reason --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index a3a75e4bbb..fb2abdd72f 100644 --- a/index.js +++ b/index.js @@ -231,7 +231,6 @@ const execa = (file, args, options) => { setTimeout(() => { try { - originalKill(0); originalKill('SIGKILL'); } catch (_) { // All good - process killed From 13c3ed177360cc8061df2bdfdc7bb7e166b4694f Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 29 Apr 2019 21:24:04 +0200 Subject: [PATCH 05/51] Fixup: Impove signal checking based on types and node defaults --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fb2abdd72f..c448c810c7 100644 --- a/index.js +++ b/index.js @@ -223,7 +223,9 @@ const execa = (file, args, options) => { const originalKill = spawned.kill.bind(spawned); spawned.kill = (signal, options) => { - if (signal === undefined || signal === 'SIGTERM') { + if (signal === undefined || + (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || + (typeof signal === 'number' && signal === os.constants.signals.SIGTERM)) { const retryAfter = options !== undefined && ['number', 'string'].includes(typeof options.retryAfter) ? Number(options.retryAfter) : From 6bf3aea44c0019becf9f5acb2b77ebc8ae3a3e3e Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 29 Apr 2019 21:32:14 +0200 Subject: [PATCH 06/51] Fixup: Increase default timeout for process termination --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index c448c810c7..ed89946d2f 100644 --- a/index.js +++ b/index.js @@ -229,7 +229,7 @@ const execa = (file, args, options) => { const retryAfter = options !== undefined && ['number', 'string'].includes(typeof options.retryAfter) ? Number(options.retryAfter) : - 2000; + 5000; setTimeout(() => { try { From 90d72a12b8de36cc660a34002ca313b66fa5f703 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 29 Apr 2019 21:41:05 +0200 Subject: [PATCH 07/51] Fixup: Add possibility to disable the timeout --- index.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index ed89946d2f..a5cba42198 100644 --- a/index.js +++ b/index.js @@ -231,13 +231,18 @@ const execa = (file, args, options) => { Number(options.retryAfter) : 5000; - setTimeout(() => { - try { - originalKill('SIGKILL'); - } catch (_) { - // All good - process killed - } - }, retryAfter); + const retry = !(options !== undefined && options.retry === false); + + if (retry) { + setTimeout(() => { + try { + originalKill('SIGKILL'); + } catch (_) { + // All good - process killed + } + }, retryAfter); + } + return originalKill(signal); } From 856896d6ba8550387b5880cb8c5b86a74fd2b28f Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 29 Apr 2019 21:48:42 +0200 Subject: [PATCH 08/51] Fixup: Imporove timeout behavior by unref'ing --- index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index a5cba42198..272944d73f 100644 --- a/index.js +++ b/index.js @@ -233,17 +233,20 @@ const execa = (file, args, options) => { const retry = !(options !== undefined && options.retry === false); - if (retry) { - setTimeout(() => { + const killResult = originalKill(signal); + + if (retry && killResult) { + const safetyTimeout = setTimeout(() => { try { originalKill('SIGKILL'); } catch (_) { // All good - process killed } }, retryAfter); + safetyTimeout.unref(); } - return originalKill(signal); + return killResult; } return originalKill(signal); From cff5e2fd9818921b4b21d61eddb659fab2a76bda Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 1 May 2019 09:20:54 +0200 Subject: [PATCH 09/51] Fixup: Move retryAfter to more appropriate place --- index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 272944d73f..090c41dc3b 100644 --- a/index.js +++ b/index.js @@ -226,16 +226,15 @@ const execa = (file, args, options) => { if (signal === undefined || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || (typeof signal === 'number' && signal === os.constants.signals.SIGTERM)) { - const retryAfter = options !== undefined && - ['number', 'string'].includes(typeof options.retryAfter) ? - Number(options.retryAfter) : - 5000; - const retry = !(options !== undefined && options.retry === false); const killResult = originalKill(signal); if (retry && killResult) { + const retryAfter = options !== undefined && + ['number', 'string'].includes(typeof options.retryAfter) ? + Number(options.retryAfter) : + 5000; const safetyTimeout = setTimeout(() => { try { originalKill('SIGKILL'); From 250498fec8d0d413e5d1b18e44171869cc4595fd Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 1 May 2019 09:33:40 +0200 Subject: [PATCH 10/51] Fixup: Remove wrong comment --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index 090c41dc3b..a515015a8e 100644 --- a/index.js +++ b/index.js @@ -239,7 +239,6 @@ const execa = (file, args, options) => { try { originalKill('SIGKILL'); } catch (_) { - // All good - process killed } }, retryAfter); safetyTimeout.unref(); From fc0d7b298ab0e18f33ca95f61032fe2173a93644 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 1 May 2019 09:36:49 +0200 Subject: [PATCH 11/51] Fixup: call `originalKill` only once --- index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index a515015a8e..5d4f1b19a9 100644 --- a/index.js +++ b/index.js @@ -223,13 +223,12 @@ const execa = (file, args, options) => { const originalKill = spawned.kill.bind(spawned); spawned.kill = (signal, options) => { + const killResult = originalKill(signal); if (signal === undefined || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || (typeof signal === 'number' && signal === os.constants.signals.SIGTERM)) { const retry = !(options !== undefined && options.retry === false); - const killResult = originalKill(signal); - if (retry && killResult) { const retryAfter = options !== undefined && ['number', 'string'].includes(typeof options.retryAfter) ? @@ -243,11 +242,9 @@ const execa = (file, args, options) => { }, retryAfter); safetyTimeout.unref(); } - - return killResult; } - return originalKill(signal); + return killResult; }; // #115 From 20e5ecfbf8546b5dde5cc57bd1399565cdc12700 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 1 May 2019 09:48:55 +0200 Subject: [PATCH 12/51] Add nice documentation for the new behavior --- readme.md | 27 +++++++++++++++++++++++++++ test.js | 5 +++++ 2 files changed, 32 insertions(+) diff --git a/readme.md b/readme.md index 9edfdaf420..85ceba7c9c 100644 --- a/readme.md +++ b/readme.md @@ -112,6 +112,14 @@ try { } */ } + +// Killing a spawned process gently, then with more force +const subprocess = execa('node'); // Lets assume node doesn't react to SIGTERM +setTimeout(() => { + subprocess.kill('SIGKILL', { + retryAfter: 2000, + }); // Attempts a kill with SIGKILL after 2 secs +}, 1000); ``` @@ -128,6 +136,7 @@ Unless the [`shell`](#shell) option is used, no shell interpreter (Bash, `cmd.ex Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which: - is also a `Promise` resolving or rejecting with a [`childProcessResult`](#childProcessResult). - exposes the following additional methods and properties. +The spawned process can be canceled with the `.cancel()` method on the promise, which causes the promise to reject an error with a `.isCanceled = true` property, provided the process gets canceled. The process will not be canceled if it has already exited. #### cancel() @@ -407,6 +416,24 @@ Default: `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`. +### child_process.kill([signal], [options]) + +You may kill the child process with this function. It is slightly different that the original function. +This one allows you to kill the process with `SIGTERM` and after a specified timeout (5 seconds) a `SIGKILL` is invoked to ensure killing the process. + +#### options.retry + +Type: `boolean`
+Default: true + +Tells the kill to start a second attemp to kill. + +#### options.retryAfter + +Type: `string | number`
+Default: 5000 + +Specifies a timeout in ms. This one defines the delay until you try to kill the pocess again. ## Tips diff --git a/test.js b/test.js index aa74acd5e4..999d6a20f8 100644 --- a/test.js +++ b/test.js @@ -138,6 +138,11 @@ test('stripFinalNewline in sync mode', t => { t.is(stdout, 'foo'); }); +test('stripFinalNewline on failure', async t => { + const {stderr} = await t.throwsAsync(execa('noop-throw', ['foo'], {stripFinalNewline: true})); + t.is(stderr, 'foo'); +}); + test('stripFinalNewline in sync mode on failure', t => { const {stderr} = t.throws(() => { execa.sync('noop-throw', ['foo'], {stripFinalNewline: true}); From d2963e58d1a36d2dbde07d10a1cf9697464d2c82 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 1 May 2019 10:39:51 +0200 Subject: [PATCH 13/51] Add broken tests that need more care --- fixtures/sub-process-no-killable | 10 +++++++ test.js | 45 +++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 fixtures/sub-process-no-killable diff --git a/fixtures/sub-process-no-killable b/fixtures/sub-process-no-killable new file mode 100644 index 0000000000..0db6c24a5d --- /dev/null +++ b/fixtures/sub-process-no-killable @@ -0,0 +1,10 @@ +#!/usr/bin/env node +'use strict'; + +process.on('SIGTERM', () => { + console.log('got sigterm but we ignore it'); +}); + +setInterval(() => { + // run forever +}, 5000); diff --git a/test.js b/test.js index 999d6a20f8..721f0b5627 100644 --- a/test.js +++ b/test.js @@ -128,9 +128,48 @@ test('stripFinalNewline: false', async t => { t.is(stdout, 'foo\n'); }); -test('stripFinalNewline on failure', async t => { - const {stderr} = await t.throwsAsync(execa('noop-throw', ['foo'], {stripFinalNewline: true})); - t.is(stderr, 'foo'); +test.cb('execa() with .kill after it with SIGKILL should kill cleanly', t => { + const proc = execa('node', ['fixtures/sub-process-no-killable']); + t.is(typeof proc.pid, 'number'); + setTimeout(() => { + proc.kill('SIGKILL'); + }, 100); + + setTimeout(() => { + t.false(isRunning(proc.pid)); + t.end(); + }, 2000); +}); + +test.cb('execa() with .kill after it with SIGTERM should not kill (no retry)', t => { + const proc = execa('node', ['fixtures/sub-process-no-killable']); + t.is(typeof proc.pid, 'number'); + setTimeout(() => { + proc.kill('SIGTERM', { + retry: false, + retryAfter: 50 + }); + }, 100); + + setTimeout(() => { + t.true(isRunning(proc.pid)); + t.end(); + }, 200); +}); + +test.cb('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', t => { + const proc = execa('node', ['fixtures/sub-process-no-killable']); + t.is(typeof proc.pid, 'number'); + setTimeout(() => { + proc.kill('SIGTERM', { + retryAfter: 50 + }); + }, 100); + + setTimeout(() => { + t.false(isRunning(proc.pid)); + t.end(); + }, 200); }); test('stripFinalNewline in sync mode', t => { From f6526aa816787dda455dd6dd8c9bec123d636a74 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Fri, 3 May 2019 20:05:27 +0200 Subject: [PATCH 14/51] Fixup: Improve the readme and follow suggestions --- readme.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 85ceba7c9c..9f13d867a9 100644 --- a/readme.md +++ b/readme.md @@ -138,6 +138,7 @@ Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#c - exposes the following additional methods and properties. The spawned process can be canceled with the `.cancel()` method on the promise, which causes the promise to reject an error with a `.isCanceled = true` property, provided the process gets canceled. The process will not be canceled if it has already exited. +The spawned process can be killed with `.kill([signal], [options])` which allows you to retry the process killing at a later time (default: after 5 seconds) #### 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`. @@ -419,21 +420,21 @@ If `true`, no quoting or escaping of arguments is done on Windows. Ignored on ot ### child_process.kill([signal], [options]) You may kill the child process with this function. It is slightly different that the original function. -This one allows you to kill the process with `SIGTERM` and after a specified timeout (5 seconds) a `SIGKILL` is invoked to ensure killing the process. ++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`. #### options.retry Type: `boolean`
Default: true -Tells the kill to start a second attemp to kill. +If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. #### options.retryAfter Type: `string | number`
Default: 5000 -Specifies a timeout in ms. This one defines the delay until you try to kill the pocess again. +How long to wait for the child process to terminate before sending `SIGKILL`. ## Tips From 91f755dfd27b1cb3bd94d61fc071e029bac0b129 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Fri, 3 May 2019 20:07:25 +0200 Subject: [PATCH 15/51] Fixup: Remove redundant sentence --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 9f13d867a9..16be60a50d 100644 --- a/readme.md +++ b/readme.md @@ -419,8 +419,7 @@ If `true`, no quoting or escaping of arguments is done on Windows. Ignored on ot ### child_process.kill([signal], [options]) -You may kill the child process with this function. It is slightly different that the original function. -+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`. +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`. #### options.retry From 371df69f8cf6705c845e7e51f1c2ed083157c1a1 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Fri, 3 May 2019 20:09:52 +0200 Subject: [PATCH 16/51] Fixup: Remove redundant asserts to `proc.pid` --- test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test.js b/test.js index 721f0b5627..932f8f4390 100644 --- a/test.js +++ b/test.js @@ -130,7 +130,6 @@ test('stripFinalNewline: false', async t => { test.cb('execa() with .kill after it with SIGKILL should kill cleanly', t => { const proc = execa('node', ['fixtures/sub-process-no-killable']); - t.is(typeof proc.pid, 'number'); setTimeout(() => { proc.kill('SIGKILL'); }, 100); @@ -143,7 +142,6 @@ test.cb('execa() with .kill after it with SIGKILL should kill cleanly', t => { test.cb('execa() with .kill after it with SIGTERM should not kill (no retry)', t => { const proc = execa('node', ['fixtures/sub-process-no-killable']); - t.is(typeof proc.pid, 'number'); setTimeout(() => { proc.kill('SIGTERM', { retry: false, @@ -159,7 +157,6 @@ test.cb('execa() with .kill after it with SIGTERM should not kill (no retry)', t test.cb('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', t => { const proc = execa('node', ['fixtures/sub-process-no-killable']); - t.is(typeof proc.pid, 'number'); setTimeout(() => { proc.kill('SIGTERM', { retryAfter: 50 From 702cbf36d5b81c6ba468b7d2c7f300331516dfe5 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Fri, 3 May 2019 20:38:52 +0200 Subject: [PATCH 17/51] Fixup: Impove tests and align them to the others --- fixtures/sub-process-no-killable | 2 +- test.js | 75 +++++++++++++++++--------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/fixtures/sub-process-no-killable b/fixtures/sub-process-no-killable index 0db6c24a5d..e773fa7eac 100644 --- a/fixtures/sub-process-no-killable +++ b/fixtures/sub-process-no-killable @@ -7,4 +7,4 @@ process.on('SIGTERM', () => { setInterval(() => { // run forever -}, 5000); +}, 20000); diff --git a/test.js b/test.js index 932f8f4390..837c21f6e5 100644 --- a/test.js +++ b/test.js @@ -128,45 +128,50 @@ test('stripFinalNewline: false', async t => { t.is(stdout, 'foo\n'); }); -test.cb('execa() with .kill after it with SIGKILL should kill cleanly', t => { - const proc = execa('node', ['fixtures/sub-process-no-killable']); - setTimeout(() => { - proc.kill('SIGKILL'); - }, 100); - - setTimeout(() => { - t.false(isRunning(proc.pid)); - t.end(); - }, 2000); -}); - -test.cb('execa() with .kill after it with SIGTERM should not kill (no retry)', t => { - const proc = execa('node', ['fixtures/sub-process-no-killable']); - setTimeout(() => { - proc.kill('SIGTERM', { - retry: false, - retryAfter: 50 - }); - }, 100); +test('execa() with .kill after it with SIGKILL should kill cleanly', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable']); + + subprocess.kill('SIGKILL'); - setTimeout(() => { - t.true(isRunning(proc.pid)); - t.end(); - }, 200); + try { + await subprocess; + t.fail('Didnt expect success'); + } catch (error) { + t.deepEqual(error.signal, 'SIGKILL'); + } }); -test.cb('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', t => { - const proc = execa('node', ['fixtures/sub-process-no-killable']); - setTimeout(() => { - proc.kill('SIGTERM', { - retryAfter: 50 - }); - }, 100); +test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable']); + + subprocess.kill('SIGTERM', { + retry: false, + retryAfter: 50 + }); + + // Weird test - The process should not die but it does... - setTimeout(() => { - t.false(isRunning(proc.pid)); - t.end(); - }, 200); + try { + await subprocess; + t.fail('Didnt expect success'); + } catch (error) { + t.deepEqual(error.signal, 'SIGTERM'); + } +}); + +test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable']); + + subprocess.kill('SIGTERM', { + retryAfter: 50 + }); + + try { + await subprocess; + t.fail('Didnt expect success'); + } catch (error) { + t.deepEqual(error.signal, 'SIGTERM'); + } }); test('stripFinalNewline in sync mode', t => { From a84be975f06ad4fd3f6cf92840c2f379be584d6e Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 5 May 2019 11:13:53 +0200 Subject: [PATCH 18/51] Fixup: Improve readme with proper formatting --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 16be60a50d..0bdb170156 100644 --- a/readme.md +++ b/readme.md @@ -424,14 +424,14 @@ Same as the original [`child_process.kill()`](https://nodejs.org/api/child_proce #### options.retry Type: `boolean`
-Default: true +Default: `true` If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. #### options.retryAfter Type: `string | number`
-Default: 5000 +Default: `5000` How long to wait for the child process to terminate before sending `SIGKILL`. From 3cb8dd0de4c2a3d06d37c1f6d7cf758fd4f29248 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 5 May 2019 11:19:48 +0200 Subject: [PATCH 19/51] Fixup: Use shallow comparison and simplify tests --- test.js | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/test.js b/test.js index 837c21f6e5..d93b5efa0a 100644 --- a/test.js +++ b/test.js @@ -133,12 +133,8 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => subprocess.kill('SIGKILL'); - try { - await subprocess; - t.fail('Didnt expect success'); - } catch (error) { - t.deepEqual(error.signal, 'SIGKILL'); - } + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGKILL'); }); test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { @@ -151,12 +147,8 @@ test('execa() with .kill after it with SIGTERM should not kill (no retry)', asyn // Weird test - The process should not die but it does... - try { - await subprocess; - t.fail('Didnt expect success'); - } catch (error) { - t.deepEqual(error.signal, 'SIGTERM'); - } + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGTERM'); }); test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { @@ -166,12 +158,8 @@ test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGK retryAfter: 50 }); - try { - await subprocess; - t.fail('Didnt expect success'); - } catch (error) { - t.deepEqual(error.signal, 'SIGTERM'); - } + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGTERM'); }); test('stripFinalNewline in sync mode', t => { From 904154adca2b4b0826a407a05b0f0f028654c55f Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 5 May 2019 11:43:52 +0200 Subject: [PATCH 20/51] Add proper typescript definitions to the new function --- index.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/index.d.ts b/index.d.ts index beee76b01d..b724663670 100644 --- a/index.d.ts +++ b/index.d.ts @@ -288,6 +288,13 @@ declare namespace execa { 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`. */ cancel(): void; + + /** + * Kills the subprocess. + * + * Causes the promise to reject an error with a `.isKilled = true` property. + */ + kill(signal?: string | undefined, options?: killOptions | undefined): void; } type ExecaChildProcess = ChildProcess & @@ -295,6 +302,11 @@ declare namespace execa { Promise>; } +interface killOptions { + retry: boolean; + retryAfter: number; +} + declare const execa: { /** Execute a file. From c6c3873121283a8107b578da8c7a5cfe85d8ea79 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 5 May 2019 18:46:58 +0200 Subject: [PATCH 21/51] Fixup: Repair the tests and wait for proper state of the subprocess --- fixtures/sub-process-no-killable | 4 ++- test.js | 46 +++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/fixtures/sub-process-no-killable b/fixtures/sub-process-no-killable index e773fa7eac..3fca9fcb3e 100644 --- a/fixtures/sub-process-no-killable +++ b/fixtures/sub-process-no-killable @@ -5,6 +5,8 @@ process.on('SIGTERM', () => { console.log('got sigterm but we ignore it'); }); +process.send('process is launched'); + setInterval(() => { - // run forever + // Run forever }, 20000); diff --git a/test.js b/test.js index d93b5efa0a..0ccaf69a27 100644 --- a/test.js +++ b/test.js @@ -14,6 +14,14 @@ process.env.FOO = 'foo'; const TIMEOUT_REGEXP = /timed out after/; +const waitForProcessLaunch = subprocess => { + return new Promise(resolve => { + subprocess.once('message', _ => { + resolve(); + }); + }); +}; + const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); test('execa()', async t => { @@ -129,8 +137,11 @@ test('stripFinalNewline: false', async t => { }); test('execa() with .kill after it with SIGKILL should kill cleanly', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable']); + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); + await waitForProcessLaunch(subprocess); subprocess.kill('SIGKILL'); const {signal} = await t.throwsAsync(subprocess); @@ -138,28 +149,49 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => }); test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable']); + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); + + await waitForProcessLaunch(subprocess); subprocess.kill('SIGTERM', { retry: false, retryAfter: 50 }); - // Weird test - The process should not die but it does... + t.true(isRunning(subprocess.pid)); + subprocess.kill('SIGKILL'); +}); + +test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); + + await waitForProcessLaunch(subprocess); + + subprocess.kill('SIGTERM', { + retryAfter: 50 + }); const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGTERM'); + t.is(signal, 'SIGKILL'); }); -test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable']); +test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); + + await waitForProcessLaunch(subprocess); subprocess.kill('SIGTERM', { retryAfter: 50 }); const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGTERM'); + t.is(signal, 'SIGKILL'); }); test('stripFinalNewline in sync mode', t => { From 7b00bbd55958d0df00942746ff7574bd960287c2 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sun, 5 May 2019 20:26:39 +0200 Subject: [PATCH 22/51] Fixup: Avoid direct Promise building and promisify Have the undefined test actually test undefined --- fixtures/sub-process-no-killable | 2 +- test.js | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/fixtures/sub-process-no-killable b/fixtures/sub-process-no-killable index 3fca9fcb3e..f1f0d6219d 100644 --- a/fixtures/sub-process-no-killable +++ b/fixtures/sub-process-no-killable @@ -5,7 +5,7 @@ process.on('SIGTERM', () => { console.log('got sigterm but we ignore it'); }); -process.send('process is launched'); +process.send(''); setInterval(() => { // Run forever diff --git a/test.js b/test.js index 0ccaf69a27..7ff99538be 100644 --- a/test.js +++ b/test.js @@ -1,3 +1,4 @@ +import {promisify} from 'util'; import path from 'path'; import fs from 'fs'; import stream from 'stream'; @@ -15,11 +16,7 @@ process.env.FOO = 'foo'; const TIMEOUT_REGEXP = /timed out after/; const waitForProcessLaunch = subprocess => { - return new Promise(resolve => { - subprocess.once('message', _ => { - resolve(); - }); - }); + return promisify(subprocess.once.bind(subprocess))('message'); }; const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); @@ -142,6 +139,7 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => }); await waitForProcessLaunch(subprocess); + subprocess.kill('SIGKILL'); const {signal} = await t.throwsAsync(subprocess); @@ -171,9 +169,7 @@ test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGK await waitForProcessLaunch(subprocess); - subprocess.kill('SIGTERM', { - retryAfter: 50 - }); + subprocess.kill(); const {signal} = await t.throwsAsync(subprocess); t.is(signal, 'SIGKILL'); From 83345ad660b80dccbd9f68b47500e60f30157736 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Mon, 6 May 2019 21:01:28 +0200 Subject: [PATCH 23/51] Fixup: Repair tests on Windows (add conditionals in test) --- test.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test.js b/test.js index 7ff99538be..9493194c95 100644 --- a/test.js +++ b/test.js @@ -158,8 +158,13 @@ test('execa() with .kill after it with SIGTERM should not kill (no retry)', asyn retryAfter: 50 }); - t.true(isRunning(subprocess.pid)); - subprocess.kill('SIGKILL'); + if (process.platform === 'win32') { + // Windows doesn't support sending signals. No re-emulates them down to SIGKILL + t.false(isRunning(subprocess.pid)); + } else { + t.true(isRunning(subprocess.pid)); + subprocess.kill('SIGKILL'); + } }); test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { @@ -172,7 +177,13 @@ test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGK subprocess.kill(); const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); + + if (process.platform === 'win32') { + // SIGTERM doesn't exist on Windows but it's emulated to send that. + t.is(signal, 'SIGTERM'); + } else { + t.is(signal, 'SIGKILL'); + } }); test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { @@ -187,7 +198,12 @@ test('execa() with .kill after it with nothing (undefined) should kill after 50 }); const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); + if (process.platform === 'win32') { + // SIGTERM doesn't exist on Windows but it's emulated to send that. + t.is(signal, 'SIGTERM'); + } else { + t.is(signal, 'SIGKILL'); + } }); test('stripFinalNewline in sync mode', t => { From 068fbb60cb8ae58492330ccf89830484b609abf4 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 15 May 2019 21:24:26 +0200 Subject: [PATCH 24/51] Fixup: Skip tests if we are on Windows --- test.js | 113 ++++++++++++++++++-------------------------------------- 1 file changed, 36 insertions(+), 77 deletions(-) diff --git a/test.js b/test.js index 9493194c95..f9c96cb580 100644 --- a/test.js +++ b/test.js @@ -56,40 +56,6 @@ test('stdout/stderr/all available on errors', async t => { t.is(typeof all, 'string'); }); -test('stdout/stderr/all are undefined if ignored', async t => { - const {stdout, stderr, all} = await execa('noop', {stdio: 'ignore'}); - t.is(stdout, undefined); - t.is(stderr, undefined); - t.is(all, undefined); -}); - -test('stdout/stderr/all are undefined if ignored in sync mode', t => { - const {stdout, stderr, all} = execa.sync('noop', {stdio: 'ignore'}); - t.is(stdout, undefined); - t.is(stderr, undefined); - t.is(all, undefined); -}); - -const WRONG_COMMAND_STDERR = process.platform === 'win32' ? - '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.' : - ''; - -test('stdout/stderr/all on process errors', async t => { - const {stdout, stderr, all} = await t.throwsAsync(execa('wrong command')); - t.is(stdout, ''); - t.is(stderr, WRONG_COMMAND_STDERR); - t.is(all, WRONG_COMMAND_STDERR); -}); - -test('stdout/stderr/all on process errors, in sync mode', t => { - const {stdout, stderr, all} = t.throws(() => { - execa.sync('wrong command'); - }); - t.is(stdout, ''); - t.is(stderr, WRONG_COMMAND_STDERR); - t.is(all, undefined); -}); - test('pass `stdout` to a file descriptor', async t => { const file = tempfile('.txt'); await execa('fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')}); @@ -146,65 +112,58 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => t.is(signal, 'SIGKILL'); }); -test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { - stdio: ['ipc'] - }); +// Windows doesn't support sending signals. No re-emulates them down to SIGKILL +if (process.platform !== 'win32') { + test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); - await waitForProcessLaunch(subprocess); + await waitForProcessLaunch(subprocess); - subprocess.kill('SIGTERM', { - retry: false, - retryAfter: 50 - }); + subprocess.kill('SIGTERM', { + retry: false, + retryAfter: 50 + }); - if (process.platform === 'win32') { - // Windows doesn't support sending signals. No re-emulates them down to SIGKILL - t.false(isRunning(subprocess.pid)); - } else { t.true(isRunning(subprocess.pid)); subprocess.kill('SIGKILL'); - } -}); - -test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { - stdio: ['ipc'] }); +} - await waitForProcessLaunch(subprocess); +// Windows doesn't support sending signals. No re-emulates them down to SIGKILL +if (process.platform !== 'win32') { + test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); - subprocess.kill(); + await waitForProcessLaunch(subprocess); - const {signal} = await t.throwsAsync(subprocess); + subprocess.kill('SIGTERM', { + retryAfter: 50 + }); - if (process.platform === 'win32') { - // SIGTERM doesn't exist on Windows but it's emulated to send that. - t.is(signal, 'SIGTERM'); - } else { + const {signal} = await t.throwsAsync(subprocess); t.is(signal, 'SIGKILL'); - } -}); - -test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { - stdio: ['ipc'] }); +} - await waitForProcessLaunch(subprocess); +// Windows doesn't support sending signals. No re-emulates them down to SIGKILL +if (process.platform !== 'win32') { + test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { + const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + stdio: ['ipc'] + }); - subprocess.kill('SIGTERM', { - retryAfter: 50 - }); + await waitForProcessLaunch(subprocess); - const {signal} = await t.throwsAsync(subprocess); - if (process.platform === 'win32') { - // SIGTERM doesn't exist on Windows but it's emulated to send that. - t.is(signal, 'SIGTERM'); - } else { + subprocess.kill(); + + const {signal} = await t.throwsAsync(subprocess); t.is(signal, 'SIGKILL'); - } -}); + }); +} test('stripFinalNewline in sync mode', t => { const {stdout} = execa.sync('noop', ['foo'], {stripFinalNewline: true}); From c2da4d29631479deaf3ab5969bd03fbd4f0e2fb5 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 15 May 2019 21:30:28 +0200 Subject: [PATCH 25/51] Fixup: Improve readme --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 0bdb170156..dddec94074 100644 --- a/readme.md +++ b/readme.md @@ -113,12 +113,12 @@ try { */ } -// Killing a spawned process gently, then with more force -const subprocess = execa('node'); // Lets assume node doesn't react to SIGTERM +// Kill a process with SIGTERM and after 2 seconds, kill it with SIGKILL +const subprocess = execa('node'); setTimeout(() => { - subprocess.kill('SIGKILL', { + subprocess.kill('SIGTERM', { retryAfter: 2000, - }); // Attempts a kill with SIGKILL after 2 secs + }); }, 1000); ``` From 61e249172d40346611e9553a53ec1698d732dd4f Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 15 May 2019 21:32:51 +0200 Subject: [PATCH 26/51] Fixup: Make test code better readable --- test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test.js b/test.js index f9c96cb580..64a23cc731 100644 --- a/test.js +++ b/test.js @@ -1,4 +1,3 @@ -import {promisify} from 'util'; import path from 'path'; import fs from 'fs'; import stream from 'stream'; @@ -16,7 +15,7 @@ process.env.FOO = 'foo'; const TIMEOUT_REGEXP = /timed out after/; const waitForProcessLaunch = subprocess => { - return promisify(subprocess.once.bind(subprocess))('message'); + return pEvent(subprocess, 'message'); }; const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); From f024c12fda5ff1d94536cd95f237fef9ecb16ff7 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Wed, 15 May 2019 21:36:00 +0200 Subject: [PATCH 27/51] Fixup: Rename fixture to something without `sub-process` --- fixtures/{sub-process-no-killable => no-killable} | 0 test.js | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename fixtures/{sub-process-no-killable => no-killable} (100%) diff --git a/fixtures/sub-process-no-killable b/fixtures/no-killable similarity index 100% rename from fixtures/sub-process-no-killable rename to fixtures/no-killable diff --git a/test.js b/test.js index 64a23cc731..ac81e9fb2c 100644 --- a/test.js +++ b/test.js @@ -99,7 +99,7 @@ test('stripFinalNewline: false', async t => { }); test('execa() with .kill after it with SIGKILL should kill cleanly', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -114,7 +114,7 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => // Windows doesn't support sending signals. No re-emulates them down to SIGKILL if (process.platform !== 'win32') { test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -133,7 +133,7 @@ if (process.platform !== 'win32') { // Windows doesn't support sending signals. No re-emulates them down to SIGKILL if (process.platform !== 'win32') { test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -151,7 +151,7 @@ if (process.platform !== 'win32') { // Windows doesn't support sending signals. No re-emulates them down to SIGKILL if (process.platform !== 'win32') { test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { - const subprocess = execa('node', ['fixtures/sub-process-no-killable'], { + const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); From ef944a673aeec0b11b97e780b32a490fe38f229a Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 16 May 2019 18:08:20 +0200 Subject: [PATCH 28/51] Fixup: Combine test runs with win32 exclusion --- test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test.js b/test.js index ac81e9fb2c..d42b4810e7 100644 --- a/test.js +++ b/test.js @@ -128,10 +128,7 @@ if (process.platform !== 'win32') { t.true(isRunning(subprocess.pid)); subprocess.kill('SIGKILL'); }); -} -// Windows doesn't support sending signals. No re-emulates them down to SIGKILL -if (process.platform !== 'win32') { test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] @@ -146,10 +143,7 @@ if (process.platform !== 'win32') { const {signal} = await t.throwsAsync(subprocess); t.is(signal, 'SIGKILL'); }); -} -// Windows doesn't support sending signals. No re-emulates them down to SIGKILL -if (process.platform !== 'win32') { test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] From 22061ec01543fbad6bab8241ba7ba05bb29daaf6 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 16 May 2019 18:11:50 +0200 Subject: [PATCH 29/51] Fixup: Reorder tests and drop simple function --- test.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/test.js b/test.js index d42b4810e7..5666df84d8 100644 --- a/test.js +++ b/test.js @@ -14,10 +14,6 @@ process.env.FOO = 'foo'; const TIMEOUT_REGEXP = /timed out after/; -const waitForProcessLaunch = subprocess => { - return pEvent(subprocess, 'message'); -}; - const getExitRegExp = exitMessage => new RegExp(`failed with exit code ${exitMessage}`); test('execa()', async t => { @@ -88,22 +84,12 @@ test('skip throwing when using reject option in sync mode', t => { t.is(exitCode, 2); }); -test('stripFinalNewline: true', async t => { - const {stdout} = await execa('noop', ['foo']); - t.is(stdout, 'foo'); -}); - -test('stripFinalNewline: false', async t => { - const {stdout} = await execa('noop', ['foo'], {stripFinalNewline: false}); - t.is(stdout, 'foo\n'); -}); - test('execa() with .kill after it with SIGKILL should kill cleanly', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); - await waitForProcessLaunch(subprocess); + await pEvent(subprocess, 'message'); subprocess.kill('SIGKILL'); @@ -118,7 +104,7 @@ if (process.platform !== 'win32') { stdio: ['ipc'] }); - await waitForProcessLaunch(subprocess); + await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', { retry: false, @@ -134,7 +120,7 @@ if (process.platform !== 'win32') { stdio: ['ipc'] }); - await waitForProcessLaunch(subprocess); + await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', { retryAfter: 50 @@ -149,7 +135,7 @@ if (process.platform !== 'win32') { stdio: ['ipc'] }); - await waitForProcessLaunch(subprocess); + await pEvent(subprocess, 'message'); subprocess.kill(); @@ -158,6 +144,16 @@ if (process.platform !== 'win32') { }); } +test('stripFinalNewline: true', async t => { + const {stdout} = await execa('noop', ['foo']); + t.is(stdout, 'foo'); +}); + +test('stripFinalNewline: false', async t => { + const {stdout} = await execa('noop', ['foo'], {stripFinalNewline: false}); + t.is(stdout, 'foo\n'); +}); + test('stripFinalNewline in sync mode', t => { const {stdout} = execa.sync('noop', ['foo'], {stripFinalNewline: true}); t.is(stdout, 'foo'); From c9ddaa63166ffbfdc0501e4105463822348163da Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 16 May 2019 18:15:13 +0200 Subject: [PATCH 30/51] Fixup: Make readme compatible with new style of wording --- readme.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/readme.md b/readme.md index dddec94074..7e2f93f3a8 100644 --- a/readme.md +++ b/readme.md @@ -136,9 +136,25 @@ Unless the [`shell`](#shell) option is used, no shell interpreter (Bash, `cmd.ex Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) which: - is also a `Promise` resolving or rejecting with a [`childProcessResult`](#childProcessResult). - exposes the following additional methods and properties. -The spawned process can be canceled with the `.cancel()` method on the promise, which causes the promise to reject an error with a `.isCanceled = true` property, provided the process gets canceled. The process will not be canceled if it has already exited. -The spawned process can be killed with `.kill([signal], [options])` which allows you to retry the process killing at a later time (default: after 5 seconds) +#### 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`. + +##### options.retry + +Type: `boolean`
+Default: `true` + +If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. + +##### options.retryAfter + +Type: `string | number`
+Default: `5000` + +How long to wait for the child process to terminate before sending `SIGKILL`. + #### 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`. @@ -417,24 +433,6 @@ Default: `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`. -### child_process.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`. - -#### options.retry - -Type: `boolean`
-Default: `true` - -If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. - -#### options.retryAfter - -Type: `string | number`
-Default: `5000` - -How long to wait for the child process to terminate before sending `SIGKILL`. - ## Tips ### Save and pipe output from a child process From b9ab61fd9ba9e1c4d2a1bb39af914fecb0b737e7 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 16 May 2019 18:19:46 +0200 Subject: [PATCH 31/51] Fixup: Reorder types and add tsdoc --- index.d.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index b724663670..5c8c5a5e96 100644 --- a/index.d.ts +++ b/index.d.ts @@ -285,16 +285,14 @@ declare namespace execa { ): Promise | ResultType>; /** - 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`. + 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`. */ - cancel(): void; + kill(signal?: string | undefined, options?: killOptions | undefined): void; /** - * Kills the subprocess. - * - * Causes the promise to reject an error with a `.isKilled = true` property. - */ - kill(signal?: string | undefined, options?: killOptions | undefined): void; + 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`. + */ + cancel(): void; } type ExecaChildProcess = ChildProcess & @@ -303,7 +301,13 @@ declare namespace execa { } interface killOptions { + /** + If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. + */ retry: boolean; + /** + How long to wait for the child process to terminate before sending `SIGKILL`. + */ retryAfter: number; } From 785c7cf5cfa516275a063b44c26ae820d61fa228 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 16 May 2019 18:30:06 +0200 Subject: [PATCH 32/51] Fixup: Remove useless spaces --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 7e2f93f3a8..3f636c802b 100644 --- a/readme.md +++ b/readme.md @@ -113,7 +113,7 @@ try { */ } -// Kill a process with SIGTERM and after 2 seconds, kill it with SIGKILL +// Kill a process with SIGTERM and after 2 seconds, kill it with SIGKILL const subprocess = execa('node'); setTimeout(() => { subprocess.kill('SIGTERM', { From 6345ca0ceb672d35b1f3a3d889c2a2a3c20ed38e Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:14:44 +0200 Subject: [PATCH 33/51] Fixup: Rephrase console message for fixture --- fixtures/no-killable | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/no-killable b/fixtures/no-killable index f1f0d6219d..3ce9ba121f 100644 --- a/fixtures/no-killable +++ b/fixtures/no-killable @@ -2,7 +2,7 @@ 'use strict'; process.on('SIGTERM', () => { - console.log('got sigterm but we ignore it'); + console.log('Received SIGTERM, but we ignore it'); }); process.send(''); From d3718d84b7b70415969379cadb51ef06787e4bbc Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:19:56 +0200 Subject: [PATCH 34/51] Fixup: Move ts type definitions to proper nnamespace --- index.d.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 5c8c5a5e96..e02b5a491f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -279,6 +279,17 @@ declare namespace execa { isCanceled: boolean; } + interface killOptions { + /** + If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. + */ + retry: boolean; + /** + How long to wait for the child process to terminate before sending `SIGKILL`. + */ + retryAfter: number; + } + interface ExecaChildPromise { catch( onRejected?: (reason: ExecaError) => ResultType | PromiseLike @@ -287,7 +298,7 @@ declare namespace execa { /** 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`. */ - kill(signal?: string | undefined, options?: killOptions | undefined): void; + kill(signal?: string, options?: execa.killOptions): void; /** 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`. @@ -300,17 +311,6 @@ declare namespace execa { Promise>; } -interface killOptions { - /** - If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. - */ - retry: boolean; - /** - How long to wait for the child process to terminate before sending `SIGKILL`. - */ - retryAfter: number; -} - declare const execa: { /** Execute a file. From 141cb53930f2a75d509b8051d5c062dd1085fef2 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:27:18 +0200 Subject: [PATCH 35/51] Fixup: Add default options, remove string as number --- index.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 5d4f1b19a9..6ed0845e1f 100644 --- a/index.js +++ b/index.js @@ -222,7 +222,7 @@ const execa = (file, args, options) => { } const originalKill = spawned.kill.bind(spawned); - spawned.kill = (signal, options) => { + spawned.kill = (signal, options = {}) => { const killResult = originalKill(signal); if (signal === undefined || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || @@ -230,17 +230,14 @@ const execa = (file, args, options) => { const retry = !(options !== undefined && options.retry === false); if (retry && killResult) { - const retryAfter = options !== undefined && - ['number', 'string'].includes(typeof options.retryAfter) ? + const retryAfter = ['number'].includes(typeof options.retryAfter) ? Number(options.retryAfter) : 5000; - const safetyTimeout = setTimeout(() => { + setTimeout(() => { try { originalKill('SIGKILL'); - } catch (_) { - } - }, retryAfter); - safetyTimeout.unref(); + } catch (_) {} + }, retryAfter).unref(); } } From 75fe83cc79f2597c47234ca4e30ecf1b9b7790c3 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:33:03 +0200 Subject: [PATCH 36/51] Fixup: Improve tsdoc wording --- index.d.ts | 10 +++++----- readme.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index e02b5a491f..1a03c426cf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -281,13 +281,13 @@ declare namespace execa { interface killOptions { /** - If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. + If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. */ - retry: boolean; + retry?: boolean; /** - How long to wait for the child process to terminate before sending `SIGKILL`. + Milliseconds to wait for the child process to terminate before sending a `SIGKILL` signal. */ - retryAfter: number; + retryAfter?: number; } interface ExecaChildPromise { @@ -296,7 +296,7 @@ declare namespace execa { ): 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`. + 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`. */ kill(signal?: string, options?: execa.killOptions): void; diff --git a/readme.md b/readme.md index 3f636c802b..7a1a659b3c 100644 --- a/readme.md +++ b/readme.md @@ -113,7 +113,7 @@ try { */ } -// Kill a process with SIGTERM and after 2 seconds, kill it with SIGKILL +// Kill a process with SIGTERM, and after 2 seconds, kill it with SIGKILL const subprocess = execa('node'); setTimeout(() => { subprocess.kill('SIGTERM', { From 86fd58f0d51e32d0164ba32770c3c3b9386e56ce Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:50:21 +0200 Subject: [PATCH 37/51] Fixup: Rename option fields `retry` => `forceKill` `retryAfter` => `forceKillAfter` --- index.d.ts | 4 ++-- index.js | 10 +++++----- readme.md | 9 ++++----- test.js | 6 +++--- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1a03c426cf..eb53eb3570 100644 --- a/index.d.ts +++ b/index.d.ts @@ -283,11 +283,11 @@ declare namespace execa { /** If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. */ - retry?: boolean; + forceKill?: boolean; /** Milliseconds to wait for the child process to terminate before sending a `SIGKILL` signal. */ - retryAfter?: number; + forceKillAfter?: number; } interface ExecaChildPromise { diff --git a/index.js b/index.js index 6ed0845e1f..a3f6cb1689 100644 --- a/index.js +++ b/index.js @@ -227,17 +227,17 @@ const execa = (file, args, options) => { if (signal === undefined || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || (typeof signal === 'number' && signal === os.constants.signals.SIGTERM)) { - const retry = !(options !== undefined && options.retry === false); + const forceKill = !(options !== undefined && options.forceKill === false); - if (retry && killResult) { - const retryAfter = ['number'].includes(typeof options.retryAfter) ? - Number(options.retryAfter) : + if (forceKill && killResult) { + const forceKillAfter = ['number'].includes(typeof options.forceKillAfter) ? + Number(options.forceKillAfter) : 5000; setTimeout(() => { try { originalKill('SIGKILL'); } catch (_) {} - }, retryAfter).unref(); + }, forceKillAfter).unref(); } } diff --git a/readme.md b/readme.md index 7a1a659b3c..d2706c9fda 100644 --- a/readme.md +++ b/readme.md @@ -117,12 +117,11 @@ try { const subprocess = execa('node'); setTimeout(() => { subprocess.kill('SIGTERM', { - retryAfter: 2000, + forceKillAfter: 2000, }); }, 1000); ``` - ## API ### execa(file, arguments, [options]) @@ -141,16 +140,16 @@ Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#c 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`. -##### options.retry +##### options.forceKill Type: `boolean`
Default: `true` If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. -##### options.retryAfter +##### options.forceKillAfter -Type: `string | number`
+Type: `string`
Default: `5000` How long to wait for the child process to terminate before sending `SIGKILL`. diff --git a/test.js b/test.js index 5666df84d8..4c9b6593b8 100644 --- a/test.js +++ b/test.js @@ -107,8 +107,8 @@ if (process.platform !== 'win32') { await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', { - retry: false, - retryAfter: 50 + forceKill: false, + forceKillAfter: 50 }); t.true(isRunning(subprocess.pid)); @@ -123,7 +123,7 @@ if (process.platform !== 'win32') { await pEvent(subprocess, 'message'); subprocess.kill('SIGTERM', { - retryAfter: 50 + forceKillAfter: 50 }); const {signal} = await t.throwsAsync(subprocess); From ed7feb5d76668aa7644c212d384be0f8f73fc931 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:53:14 +0200 Subject: [PATCH 38/51] Fixup: Improve test definition wording --- test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test.js b/test.js index 4c9b6593b8..94a816cfba 100644 --- a/test.js +++ b/test.js @@ -84,7 +84,7 @@ test('skip throwing when using reject option in sync mode', t => { t.is(exitCode, 2); }); -test('execa() with .kill after it with SIGKILL should kill cleanly', async t => { +test('execa() with .kill() after it with SIGKILL should kill cleanly', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -99,7 +99,7 @@ test('execa() with .kill after it with SIGKILL should kill cleanly', async t => // Windows doesn't support sending signals. No re-emulates them down to SIGKILL if (process.platform !== 'win32') { - test('execa() with .kill after it with SIGTERM should not kill (no retry)', async t => { + test('execa() with .kill() after it with SIGTERM should not kill (no retry)', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -115,7 +115,7 @@ if (process.platform !== 'win32') { subprocess.kill('SIGKILL'); }); - test('execa() with .kill after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { + test('execa() with .kill() after it with SIGTERM should kill after 50 ms with SIGKILL', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); @@ -130,7 +130,7 @@ if (process.platform !== 'win32') { t.is(signal, 'SIGKILL'); }); - test('execa() with .kill after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { + test('execa() with .kill() after it with nothing (undefined) should kill after 50 ms with SIGKILL', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { stdio: ['ipc'] }); From fa1502c6c4e0150ef6093482186d7bcafbd36fa1 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 12:55:11 +0200 Subject: [PATCH 39/51] Fixup: proper description why win32 exclusion in test --- test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.js b/test.js index 94a816cfba..f4f6b3e513 100644 --- a/test.js +++ b/test.js @@ -97,7 +97,8 @@ test('execa() with .kill() after it with SIGKILL should kill cleanly', async t = t.is(signal, 'SIGKILL'); }); -// Windows doesn't support sending signals. No re-emulates them down to 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('execa() with .kill() after it with SIGTERM should not kill (no retry)', async t => { const subprocess = execa('node', ['fixtures/no-killable'], { From 7bf47adfe5b04fbc9d319a5c9d5377fa5642a31c Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 13:05:09 +0200 Subject: [PATCH 40/51] Fixup: Follow readme suggestions --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index d2706c9fda..57b91d66f3 100644 --- a/readme.md +++ b/readme.md @@ -117,7 +117,7 @@ try { const subprocess = execa('node'); setTimeout(() => { subprocess.kill('SIGTERM', { - forceKillAfter: 2000, + forceKillAfter: 2000 }); }, 1000); ``` @@ -138,21 +138,21 @@ Returns a [`child_process` instance](https://nodejs.org/api/child_process.html#c #### 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`. +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`. ##### options.forceKill Type: `boolean`
Default: `true` -If the first signal does not terminate the child after a specified timout, a `SIGKILL` will be sent to the process. +If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. ##### options.forceKillAfter Type: `string`
Default: `5000` -How long to wait for the child process to terminate before sending `SIGKILL`. +Milliseconds to wait for the child process to terminate before sending `SIGKILL`. #### cancel() From 7f9c122ff9b797c54dd483c1e0e49b3435977c39 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Sat, 18 May 2019 13:27:55 +0200 Subject: [PATCH 41/51] Add test code for ts types --- index.test-d.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/index.test-d.ts b/index.test-d.ts index ef8d904015..f6854123b4 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -8,6 +8,38 @@ import { ExecaSyncError } from '.'; +try { + const execaPromise = execa('unicorns'); + execaPromise.kill(); + + const unicornsResult = await execaPromise; + expectType(unicornsResult.command); + expectType(unicornsResult.exitCode); + expectType(unicornsResult.exitCodeName); + expectType(unicornsResult.stdout); + expectType(unicornsResult.stderr); + expectType(unicornsResult.all); + expectType(unicornsResult.failed); + expectType(unicornsResult.timedOut); + expectType(unicornsResult.isCanceled); + expectType(unicornsResult.killed); + expectType(unicornsResult.signal); +} catch (error) { + const execaError: ExecaError = error; + + expectType(execaError.message); + expectType(execaError.exitCode); + expectType(execaError.exitCodeName); + expectType(execaError.stdout); + expectType(execaError.stderr); + expectType(execaError.all); + expectType(execaError.failed); + expectType(execaError.timedOut); + expectType(execaError.isCanceled); + expectType(execaError.killed); + expectType(execaError.signal); +} + try { const execaPromise = execa('unicorns'); execaPromise.cancel(); From 0fca4a1b2611992066edb7ad8192c51af3e1cbdc Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Tue, 21 May 2019 21:28:46 +0200 Subject: [PATCH 42/51] Fixup: Optimize code chunks --- index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index a3f6cb1689..f2ba7bf307 100644 --- a/index.js +++ b/index.js @@ -222,16 +222,15 @@ const execa = (file, args, options) => { } const originalKill = spawned.kill.bind(spawned); - spawned.kill = (signal, options = {}) => { + spawned.kill = (signal = 'SIGTERM', options = {}) => { const killResult = originalKill(signal); - if (signal === undefined || - (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || - (typeof signal === 'number' && signal === os.constants.signals.SIGTERM)) { - const forceKill = !(options !== undefined && options.forceKill === false); + if ((typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || + (signal === os.constants.signals.SIGTERM)) { + const forceKill = !(options.forceKill === false); if (forceKill && killResult) { - const forceKillAfter = ['number'].includes(typeof options.forceKillAfter) ? - Number(options.forceKillAfter) : + const forceKillAfter = Number.isInteger(options.forceKillAfter) ? + options.forceKillAfter : 5000; setTimeout(() => { try { From 5615e22e5ee2efb4b03470e9d4c8ab60ebd07bad Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Tue, 21 May 2019 21:34:46 +0200 Subject: [PATCH 43/51] Fixup: Add old tests that were remove on accident --- test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test.js b/test.js index f4f6b3e513..64e66f72db 100644 --- a/test.js +++ b/test.js @@ -51,6 +51,40 @@ test('stdout/stderr/all available on errors', async t => { t.is(typeof all, 'string'); }); +test('stdout/stderr/all are undefined if ignored', async t => { + const {stdout, stderr, all} = await execa('noop', {stdio: 'ignore'}); + t.is(stdout, undefined); + t.is(stderr, undefined); + t.is(all, undefined); +}); + +test('stdout/stderr/all are undefined if ignored in sync mode', t => { + const {stdout, stderr, all} = execa.sync('noop', {stdio: 'ignore'}); + t.is(stdout, undefined); + t.is(stderr, undefined); + t.is(all, undefined); +}); + +const WRONG_COMMAND_STDERR = process.platform === 'win32' ? + '\'wrong\' is not recognized as an internal or external command,\r\noperable program or batch file.' : + ''; + +test('stdout/stderr/all on process errors', async t => { + const {stdout, stderr, all} = await t.throwsAsync(execa('wrong command')); + t.is(stdout, ''); + t.is(stderr, WRONG_COMMAND_STDERR); + t.is(all, WRONG_COMMAND_STDERR); +}); + +test('stdout/stderr/all on process errors, in sync mode', t => { + const {stdout, stderr, all} = t.throws(() => { + execa.sync('wrong command'); + }); + t.is(stdout, ''); + t.is(stderr, WRONG_COMMAND_STDERR); + t.is(all, undefined); +}); + test('pass `stdout` to a file descriptor', async t => { const file = tempfile('.txt'); await execa('fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')}); From 02f5ab1ac68258ae734bf3c67033587daec93ac2 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Tue, 21 May 2019 21:54:43 +0200 Subject: [PATCH 44/51] Fixup: improve ts tests I suppose --- index.test-d.ts | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/index.test-d.ts b/index.test-d.ts index f6854123b4..653e97b251 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -8,38 +8,6 @@ import { ExecaSyncError } from '.'; -try { - const execaPromise = execa('unicorns'); - execaPromise.kill(); - - const unicornsResult = await execaPromise; - expectType(unicornsResult.command); - expectType(unicornsResult.exitCode); - expectType(unicornsResult.exitCodeName); - expectType(unicornsResult.stdout); - expectType(unicornsResult.stderr); - expectType(unicornsResult.all); - expectType(unicornsResult.failed); - expectType(unicornsResult.timedOut); - expectType(unicornsResult.isCanceled); - expectType(unicornsResult.killed); - expectType(unicornsResult.signal); -} catch (error) { - const execaError: ExecaError = error; - - expectType(execaError.message); - expectType(execaError.exitCode); - expectType(execaError.exitCodeName); - expectType(execaError.stdout); - expectType(execaError.stderr); - expectType(execaError.all); - expectType(execaError.failed); - expectType(execaError.timedOut); - expectType(execaError.isCanceled); - expectType(execaError.killed); - expectType(execaError.signal); -} - try { const execaPromise = execa('unicorns'); execaPromise.cancel(); @@ -151,6 +119,14 @@ execa('unicorns', {maxBuffer: 1000}); execa('unicorns', {killSignal: 'SIGTERM'}); execa('unicorns', {killSignal: 9}); execa('unicorns', {windowsVerbatimArguments: true}); +execa('unicorns').kill(); +execa('unicorns').kill('SIGKILL'); +execa('unicorns').kill(undefined); +execa('unicorns').kill('SIGKILL', {}); +execa('unicorns').kill('SIGKILL', {forceKill: true}); +execa('unicorns').kill('SIGKILL', {forceKill: false}); +execa('unicorns').kill('SIGKILL', {forceKillAfter: 42}); +execa('unicorns').kill('SIGKILL', {forceKillAfter: undefined}); expectType>(execa('unicorns')); expectType>(await execa('unicorns')); From 8fc7179ba1397d230085b5a7114d837d49be0183 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Tue, 21 May 2019 21:58:30 +0200 Subject: [PATCH 45/51] Fixup: More optimization for kill function --- index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index f2ba7bf307..360f51663a 100644 --- a/index.js +++ b/index.js @@ -225,10 +225,8 @@ const execa = (file, args, options) => { spawned.kill = (signal = 'SIGTERM', options = {}) => { const killResult = originalKill(signal); if ((typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || - (signal === os.constants.signals.SIGTERM)) { - const forceKill = !(options.forceKill === false); - - if (forceKill && killResult) { + signal === os.constants.signals.SIGTERM) { + if (options.forceKill !== false && killResult) { const forceKillAfter = Number.isInteger(options.forceKillAfter) ? options.forceKillAfter : 5000; From bb53e7ff3ea4e3715de125dc1d8705093e443006 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 23 May 2019 07:38:49 +0200 Subject: [PATCH 46/51] Fixup: Extract complicated if statement to own function --- index.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 360f51663a..73f3b196f2 100644 --- a/index.js +++ b/index.js @@ -209,6 +209,14 @@ function joinCommand(file, args = []) { return [file, ...args].join(' '); } +function shouldForceKill(signal, options, killResult) { + return ((typeof signal === 'string' && + signal.toUpperCase() === 'SIGTERM') || + signal === os.constants.signals.SIGTERM) && + options.forceKill !== false && + killResult; +} + const execa = (file, args, options) => { const parsed = handleArgs(file, args, options); const {encoding, buffer, maxBuffer} = parsed.options; @@ -224,18 +232,15 @@ const execa = (file, args, options) => { const originalKill = spawned.kill.bind(spawned); spawned.kill = (signal = 'SIGTERM', options = {}) => { const killResult = originalKill(signal); - if ((typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM') || - signal === os.constants.signals.SIGTERM) { - if (options.forceKill !== false && killResult) { - const forceKillAfter = Number.isInteger(options.forceKillAfter) ? - options.forceKillAfter : - 5000; - setTimeout(() => { - try { - originalKill('SIGKILL'); - } catch (_) {} - }, forceKillAfter).unref(); - } + if (shouldForceKill(signal, options, killResult)) { + const forceKillAfter = Number.isInteger(options.forceKillAfter) ? + options.forceKillAfter : + 5000; + setTimeout(() => { + try { + originalKill('SIGKILL'); + } catch (_) {} + }, forceKillAfter).unref(); } return killResult; From 50d42197f9bcf721ace190616d5c63b39da5811d Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 23 May 2019 21:18:12 +0200 Subject: [PATCH 47/51] Fixup: Add defaults in tsdoc --- index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.d.ts b/index.d.ts index eb53eb3570..10b4ecca56 100644 --- a/index.d.ts +++ b/index.d.ts @@ -282,10 +282,14 @@ declare namespace execa { interface killOptions { /** If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. + + @default true */ forceKill?: boolean; /** Milliseconds to wait for the child process to terminate before sending a `SIGKILL` signal. + + @default 5000 */ forceKillAfter?: number; } From cb8aacbca87ecc87d34a00af256f2ff9cb35bfdf Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 23 May 2019 21:23:15 +0200 Subject: [PATCH 48/51] Fixup: Undo previously added testchange of unrelated code --- test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test.js b/test.js index 64e66f72db..48df0daae4 100644 --- a/test.js +++ b/test.js @@ -189,16 +189,16 @@ test('stripFinalNewline: false', async t => { t.is(stdout, 'foo\n'); }); -test('stripFinalNewline in sync mode', t => { - const {stdout} = execa.sync('noop', ['foo'], {stripFinalNewline: true}); - t.is(stdout, 'foo'); -}); - test('stripFinalNewline on failure', async t => { const {stderr} = await t.throwsAsync(execa('noop-throw', ['foo'], {stripFinalNewline: true})); t.is(stderr, 'foo'); }); +test('stripFinalNewline in sync mode', t => { + const {stdout} = execa.sync('noop', ['foo'], {stripFinalNewline: true}); + t.is(stdout, 'foo'); +}); + test('stripFinalNewline in sync mode on failure', t => { const {stderr} = t.throws(() => { execa.sync('noop-throw', ['foo'], {stripFinalNewline: true}); From 756cb780329a670cba1bd404ad6db0d680b498a1 Mon Sep 17 00:00:00 2001 From: "Philipp Tusch zokker13@posteo.de" Date: Thu, 23 May 2019 21:25:20 +0200 Subject: [PATCH 49/51] Fixup: Undo readme change --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 57b91d66f3..9a8bb52641 100644 --- a/readme.md +++ b/readme.md @@ -432,6 +432,7 @@ Default: `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`. + ## Tips ### Save and pipe output from a child process From 454f020780662dfbe2c5f0d666c2951cda906efb Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 1 Jun 2019 13:38:59 +0700 Subject: [PATCH 50/51] Update index.d.ts --- index.d.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 10b4ecca56..fd814f0f2a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -279,13 +279,14 @@ declare namespace execa { isCanceled: boolean; } - interface killOptions { + interface KillOptions { /** - If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. + If the first signal does not terminate the child process after a specified timeout, a `SIGKILL` signal will be sent to the process. @default true */ forceKill?: boolean; + /** Milliseconds to wait for the child process to terminate before sending a `SIGKILL` signal. @@ -302,7 +303,7 @@ declare namespace execa { /** 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`. */ - kill(signal?: string, options?: execa.killOptions): void; + kill(signal?: string, options?: execa.KillOptions): void; /** 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`. From 4c1af5132962a8306143551ef86421eb5da93f0e Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 1 Jun 2019 13:41:10 +0700 Subject: [PATCH 51/51] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 9a8bb52641..0539d8c1f7 100644 --- a/readme.md +++ b/readme.md @@ -145,7 +145,7 @@ Same as the original [`child_process#kill()`](https://nodejs.org/api/child_proce Type: `boolean`
Default: `true` -If the first signal does not terminate the child after a specified timeout, a `SIGKILL` signal will be sent to the process. +If the first signal does not terminate the child process after a specified timeout, a `SIGKILL` signal will be sent to the process. ##### options.forceKillAfter