From 9bff25d0bdc48d37248d7257fbda042cb283aa05 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Sun, 18 Aug 2024 15:39:24 +0100 Subject: [PATCH] Handle stream error --- index.js | 32 ++++++++++++++++++++++---------- test.js | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 8224e45..0e2215f 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ import {spawn} from 'node:child_process'; import {once} from 'node:events'; +import {finished} from 'node:stream/promises'; import {lineIterator, combineAsyncIterators} from './utilities.js'; export default function nanoSpawn(command, commandArguments = [], options = {}) { @@ -23,32 +24,43 @@ export default function nanoSpawn(command, commandArguments = [], options = {}) const getResult = async subprocess => { const result = {}; - bufferOutput(subprocess.stdout, result, 'stdout'); - bufferOutput(subprocess.stderr, result, 'stderr'); + const onExit = waitForExit(subprocess); + const onStdoutDone = bufferOutput(subprocess.stdout, result, 'stdout'); + const onStderrDone = bufferOutput(subprocess.stderr, result, 'stderr'); try { - await once(subprocess, 'close'); + await Promise.all([onExit, onStdoutDone, onStderrDone]); return getOutput(subprocess, result); } catch (error) { - // 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. + await Promise.allSettled([onExit, onStdoutDone, onStderrDone]); + throw Object.assign(error, getOutput(subprocess, 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. +const waitForExit = async subprocess => { + try { + await once(subprocess, 'close'); + } catch (error) { if (subprocess.pid !== undefined) { await Promise.allSettled([once(subprocess, 'close')]); } - throw Object.assign(error, getOutput(subprocess, result)); + throw error; } }; -const bufferOutput = (stream, result, streamName) => { +const bufferOutput = async (stream, result, streamName) => { stream.setEncoding('utf8'); result[streamName] = ''; stream.on('data', chunk => { result[streamName] += chunk; }); + await finished(stream, {cleanup: true}); }; const getOutput = ({exitCode, signalCode}, {stdout, stderr}) => ({ diff --git a/test.js b/test.js index 8938f73..79a26f8 100644 --- a/test.js +++ b/test.js @@ -173,3 +173,17 @@ test('promise.subprocess is set', async t => { const {exitCode} = await promise; t.is(exitCode, undefined); }); + +test('Handles stdout error', async t => { + const promise = nanoSpawn('node', ['--version']); + const error = new Error(testString); + promise.subprocess.stdout.emit('error', error); + t.is(await t.throwsAsync(promise), error); +}); + +test('Handles stderr error', async t => { + const promise = nanoSpawn('node', ['--version']); + const error = new Error(testString); + promise.subprocess.stderr.emit('error', error); + t.is(await t.throwsAsync(promise), error); +});