Skip to content

Commit b548d8b

Browse files
committed
Fix verbose discarding IPC messages when its function throws
1 parent 59de065 commit b548d8b

6 files changed

Lines changed: 60 additions & 10 deletions

File tree

‎docs/debugging.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ It is called once per log line. The first argument is the default log line strin
116116

117117
If a string is returned, it is printed on `stderr`. If `undefined` is returned, nothing is printed.
118118

119+
If the function throws, the subprocess fails with that error as [`error.cause`](api.md#errorcause). If it threw while logging the output of [`stdout`](api.md#resultstdout), [`stderr`](api.md#resultstderr) or another file descriptor, that file descriptor's output is then not kept.
120+
119121
### Filter logs
120122

121123
```js

‎lib/io/contents.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const getBufferedData = async streamPromise => {
111111
};
112112

113113
// Ensure we are returning Uint8Arrays when using `encoding: 'buffer'`
114-
const handleBufferedData = ({bufferedData}) => isArrayBuffer(bufferedData)
115-
? new Uint8Array(bufferedData)
116-
: bufferedData;
114+
// The error might be any value thrown by the `verbose` function, including `null` or `undefined`
115+
const handleBufferedData = error => isArrayBuffer(error?.bufferedData)
116+
? new Uint8Array(error.bufferedData)
117+
: error?.bufferedData;

‎lib/ipc/buffer-messages.js‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const waitForIpcOutput = async ({
1919
const isVerbose = shouldLogIpc(verboseInfo);
2020
const buffer = getFdSpecificValue(bufferArray, 'ipc');
2121
const maxBuffer = getFdSpecificValue(maxBufferArray, 'ipc');
22+
// A throwing `verbose` function must not prevent the following messages from being buffered
23+
const logErrors = [];
2224

2325
for await (const message of loopOnMessages({
2426
anyProcess: subprocess,
@@ -34,10 +36,18 @@ export const waitForIpcOutput = async ({
3436
}
3537

3638
if (isVerbose) {
37-
logIpcOutput(message, verboseInfo);
39+
try {
40+
logIpcOutput(message, verboseInfo);
41+
} catch (error) {
42+
logErrors.push(error);
43+
}
3844
}
3945
}
4046

47+
if (logErrors.length > 0) {
48+
throw logErrors[0];
49+
}
50+
4151
return ipcOutput;
4252
};
4353

‎lib/verbose/log.js‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,9 @@ const getPrintedLine = verboseObject => {
4343
return {verboseLine, verboseObject};
4444
};
4545

46-
// Serialize any type to a line string, for logging
46+
// Serialize any type to a line string, for logging.
47+
// Tabs are escaped by `escapeLines()`, like any other control character.
4748
export const serializeVerboseMessage = message => {
4849
const messageString = typeof message === 'string' ? message : inspect(message);
49-
const escapedMessage = escapeLines(messageString);
50-
return escapedMessage.replaceAll('\t', () => ' '.repeat(TAB_SIZE));
50+
return escapeLines(messageString);
5151
};
52-
53-
// Same as `util.inspect()`
54-
const TAB_SIZE = 2;

‎test/verbose/custom-throw.js‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import test from 'ava';
2+
import {execa} from '../../index.js';
23
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
34
import {foobarString} from '../helpers/input.js';
45
import {runVerboseSubprocess} from '../helpers/verbose.js';
@@ -65,3 +66,37 @@ const testCommandThrowWrap = async (t, type, options, isSync) => {
6566
test('Propagate wrapped exception in "verbose" function, "output", spawn error', testCommandThrowWrap, 'output', earlyErrorOptions, false);
6667
test('Propagate wrapped exception in "verbose" function, "ipc", spawn error', testCommandThrowWrap, 'ipc', earlyErrorOptions, false);
6768
test('Propagate wrapped exception in "verbose" function, "output", spawn error, sync', testCommandThrowWrap, 'output', earlyErrorOptionsSync, true);
69+
70+
// A throwing `verbose` function is a logging error: it must not discard IPC messages which have already been buffered
71+
const getThrowingVerbose = (type, thrownValue = new Error(foobarString)) => ({
72+
verbose: (line, verboseObject) => {
73+
if (verboseObject.type === type) {
74+
throw thrownValue;
75+
}
76+
},
77+
});
78+
79+
test('IPC output is kept when the "verbose" function throws, "ipc"', async t => {
80+
const {cause, ipcOutput} = await t.throwsAsync(execa('ipc-send-twice.js', {ipc: true, ...getThrowingVerbose('ipc')}));
81+
t.is(cause.message, foobarString);
82+
t.deepEqual(ipcOutput, ['foo', 'bar']);
83+
});
84+
85+
// A `verbose` function might throw a non-Error, or a falsy value. Neither must be mistaken for no error at all
86+
test('A non-Error thrown by the "verbose" function is propagated, "output"', async t => {
87+
const {cause, stdout} = await t.throwsAsync(execa('noop.js', [foobarString], getThrowingVerbose('output', foobarString)));
88+
t.is(cause, foobarString);
89+
t.is(stdout, undefined);
90+
});
91+
92+
test('A null value thrown by the "verbose" function is propagated, "output"', async t => {
93+
const {cause, stdout} = await t.throwsAsync(execa('noop.js', [foobarString], getThrowingVerbose('output', null)));
94+
t.is(cause, null);
95+
t.is(stdout, undefined);
96+
});
97+
98+
test('IPC output is kept when the "verbose" function throws a falsy value, "ipc"', async t => {
99+
const {cause, ipcOutput} = await t.throwsAsync(execa('ipc-send-twice.js', {ipc: true, ...getThrowingVerbose('ipc', 0)}));
100+
t.is(cause, 0);
101+
t.deepEqual(ipcOutput, ['foo', 'bar']);
102+
});

‎test/verbose/output-enable.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ test('Escapes control characters from stdout', async t => {
114114
t.is(getOutputLine(stderr), `${testTimestamp} [0] \\u0001`);
115115
});
116116

117+
test('Escapes tabs from stdout', async t => {
118+
const {stderr} = await nestedSubprocess('noop.js', [`${foobarString}\t${foobarString}`], {verbose: 'full'});
119+
t.is(getOutputLine(stderr), `${testTimestamp} [0] ${foobarString}\\t${foobarString}`);
120+
});
121+
117122
const testStdioSame = async (t, fdNumber) => {
118123
const {nestedResult: {stdio}} = await nestedSubprocess('noop-fd.js', [`${fdNumber}`, foobarString], {verbose: 'full'});
119124
t.is(stdio[fdNumber], foobarString);

0 commit comments

Comments
 (0)