Skip to content

Commit

Permalink
Handle stream error
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Aug 18, 2024
1 parent 14e44ca commit 9bff25d
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 10 deletions.
32 changes: 22 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -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 = {}) {
Expand All @@ -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}) => ({
Expand Down
14 changes: 14 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

0 comments on commit 9bff25d

Please sign in to comment.