From b77520fba8c2a099ab73f709e9667d7a8b8677ab Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 18 Aug 2024 11:46:40 +0100 Subject: [PATCH] Add `result.signalName` --- index.js | 20 ++++++++++++++++---- test.js | 54 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 6171a92..aaea50e 100644 --- a/index.js +++ b/index.js @@ -27,10 +27,19 @@ const getResult = async subprocess => { bufferOutput(subprocess, result, 'stderr'); try { - const [exitCode] = await once(subprocess, 'close'); - return {...getOutput(result), exitCode}; + await once(subprocess, 'close'); + return getOutput(subprocess, result); } catch (error) { - throw Object.assign(error, getOutput(result)); + // The `error` event on subprocess is emitted either: + // - Before `spawn`, e.g. for a non-existing executable file. + // Then, `subprocess.pid` is `undefined` and `close` is never emitted. + // - After `spawn`, e.g. for the `signal` option. + // Then, `subprocess.pid` is set and `close` is always emitted. + if (subprocess.pid !== undefined) { + await Promise.allSettled([once(subprocess, 'close')]); + } + + throw Object.assign(error, getOutput(subprocess, result)); } }; @@ -41,7 +50,10 @@ const bufferOutput = (subprocess, result, streamName) => { }); }; -const getOutput = ({stdout, stderr}) => ({ +const getOutput = ({exitCode, signalCode}, {stdout, stderr}) => ({ + // `exitCode` can be a negative number (`errno`) when the `error` event is emitted on the subprocess + ...(exitCode === null || exitCode < 0 ? {} : {exitCode}), + ...(signalCode === null ? {} : {signalName: signalCode}), stdout: stripNewline(stdout), stderr: stripNewline(stderr), }); diff --git a/test.js b/test.js index cbbc5b1..309822a 100644 --- a/test.js +++ b/test.js @@ -12,13 +12,45 @@ const arrayFromAsync = async asyncIterable => { }; test('can pass options object without any arguments', async t => { - const {exitCode} = await nanoSpawn('node', {timeout: 1}); - t.is(exitCode, null); + const {exitCode, signalName} = await nanoSpawn('node', {timeout: 1}); + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); }); -test('result.exitCode is set', async t => { - const {exitCode} = await nanoSpawn('node', ['--version']); +test('result.exitCode|signalName on success', async t => { + const {exitCode, signalName} = await nanoSpawn('node', ['--version']); t.is(exitCode, 0); + t.is(signalName, undefined); +}); + +test('result.exitCode|signalName on non-0 exit code', async t => { + const {exitCode, signalName} = await nanoSpawn('node', ['-e', 'process.exit(2)']); + t.is(exitCode, 2); + t.is(signalName, undefined); +}); + +test('result.exitCode|signalName on signal termination', async t => { + const {exitCode, signalName} = await nanoSpawn('node', {timeout: 1}); + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); +}); + +test('result.exitCode|signalName on invalid child_process options', t => { + const {exitCode, signalName} = t.throws(() => nanoSpawn('node', ['--version'], {nativeOptions: {detached: 'true'}})); + t.is(exitCode, undefined); + t.is(signalName, undefined); +}); + +test('result.exitCode|signalName on "error" event before spawn', async t => { + const {exitCode, signalName} = await t.throwsAsync(nanoSpawn('non-existent-command')); + t.is(exitCode, undefined); + t.is(signalName, undefined); +}); + +test('result.exitCode|signalName on "error" event after spawn', async t => { + const {exitCode, signalName} = await t.throwsAsync(nanoSpawn('node', {signal: AbortSignal.abort()})); + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); }); test('result.stdout is set', async t => { @@ -92,6 +124,12 @@ test('stdout handles 2 newlines at the end', async t => { t.deepEqual(lines, ['Hello', 'World', '']); }); +test('returns termination signal', async t => { + const {exitCode, signalName} = await nanoSpawn('node', {timeout: 1}); + t.is(exitCode, undefined); + t.is(signalName, 'SIGTERM'); +}); + test('stdout handles Windows newlines', async t => { const result = nanoSpawn('node', ['-e', 'process.stdout.write("Hello\\r\\nWorld")']); const lines = await arrayFromAsync(result.stdout); @@ -104,12 +142,6 @@ test('stdout handles Windows newline at the end', async t => { t.deepEqual(lines, ['Hello', 'World']); }); -test('rejects on error', async t => { - await t.throwsAsync( - nanoSpawn('non-existent-command'), - ); -}); - test('returns a promise', async t => { const result = nanoSpawn('node', ['--version']); t.false(Object.prototype.propertyIsEnumerable.call(result, 'then')); @@ -123,5 +155,5 @@ test('promise.subprocess is set', async t => { promise.subprocess.kill(); const {exitCode} = await promise; - t.is(exitCode, null); + t.is(exitCode, undefined); });