Skip to content

Commit

Permalink
Allow setting different maxBuffer values for stdout/stderr (#966)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Apr 7, 2024
1 parent a5dcf42 commit d59788a
Show file tree
Hide file tree
Showing 16 changed files with 194 additions and 57 deletions.
16 changes: 9 additions & 7 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,9 +661,11 @@ type CommonOptions<IsSync extends boolean = boolean> = {
This can also be set to `'full'` by setting the `NODE_DEBUG=execa` environment variable in the current process.
By default, this applies to both `stdout` and `stderr`, but different values can also be passed.
@default 'none'
*/
readonly verbose?: 'none' | 'short' | 'full';
readonly verbose?: FdGenericOption<'none' | 'short' | 'full'>;

/**
Kill the subprocess when the current process exits unless either:
Expand Down Expand Up @@ -747,27 +749,27 @@ type CommonOptions<IsSync extends boolean = boolean> = {
/**
Subprocess options.
Some options are related to the subprocess output: `maxBuffer`. By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.
Some options are related to the subprocess output: `verbose`, `maxBuffer`. By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.
@example
```
await execa('./run.js', {maxBuffer: 1e6}) // Same value for stdout and stderr
await execa('./run.js', {maxBuffer: {stdout: 1e4, stderr: 1e6}}) // Different values
await execa('./run.js', {verbose: 'full'}) // Same value for stdout and stderr
await execa('./run.js', {verbose: {stdout: 'none', stderr: 'full'}}) // Different values
```
*/
export type Options = CommonOptions<false>;

/**
Subprocess options, with synchronous methods.
Some options are related to the subprocess output: `maxBuffer`. By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.
Some options are related to the subprocess output: `verbose`, `maxBuffer`. By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.
@example
```
execaSync('./run.js', {maxBuffer: 1e6}) // Same value for stdout and stderr
execaSync('./run.js', {maxBuffer: {stdout: 1e4, stderr: 1e6}}) // Different values
execaSync('./run.js', {verbose: 'full'}) // Same value for stdout and stderr
execaSync('./run.js', {verbose: {stdout: 'none', stderr: 'full'}}) // Different values
```
*/
export type SyncOptions = CommonOptions<true>;
Expand Down
20 changes: 20 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,26 @@ execaSync('unicorns', {maxBuffer: {fd1: 0}});
execaSync('unicorns', {maxBuffer: {fd2: 0}});
execaSync('unicorns', {maxBuffer: {fd3: 0}});
expectError(execaSync('unicorns', {maxBuffer: {stdout: '0'}}));
execa('unicorns', {verbose: {}});
expectError(execa('unicorns', {verbose: []}));
execa('unicorns', {verbose: {stdout: 'none'}});
execa('unicorns', {verbose: {stderr: 'none'}});
execa('unicorns', {verbose: {stdout: 'none', stderr: 'none'} as const});
execa('unicorns', {verbose: {all: 'none'}});
execa('unicorns', {verbose: {fd1: 'none'}});
execa('unicorns', {verbose: {fd2: 'none'}});
execa('unicorns', {verbose: {fd3: 'none'}});
expectError(execa('unicorns', {verbose: {stdout: 'other'}}));
execaSync('unicorns', {verbose: {}});
expectError(execaSync('unicorns', {verbose: []}));
execaSync('unicorns', {verbose: {stdout: 'none'}});
execaSync('unicorns', {verbose: {stderr: 'none'}});
execaSync('unicorns', {verbose: {stdout: 'none', stderr: 'none'} as const});
execaSync('unicorns', {verbose: {all: 'none'}});
execaSync('unicorns', {verbose: {fd1: 'none'}});
execaSync('unicorns', {verbose: {fd2: 'none'}});
execaSync('unicorns', {verbose: {fd3: 'none'}});
expectError(execaSync('unicorns', {verbose: {stdout: 'other'}}));

expectError(execa('unicorns', {stdio: []}));
expectError(execaSync('unicorns', {stdio: []}));
Expand Down
3 changes: 2 additions & 1 deletion lib/arguments/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {execaCoreAsync} from '../async.js';
import {execaCoreSync} from '../sync.js';
import {normalizeArguments} from './normalize.js';
import {isTemplateString, parseTemplates} from './template.js';
import {FD_SPECIFIC_OPTIONS} from './specific.js';

export const createExeca = (mapArguments, boundOptions, deepOptions, setBoundExeca) => {
const createNested = (mapArguments, boundOptions, setBoundExeca) => createExeca(mapArguments, boundOptions, deepOptions, setBoundExeca);
Expand Down Expand Up @@ -60,4 +61,4 @@ const mergeOption = (optionName, boundOptionValue, optionValue) => {
return optionValue;
};

const DEEP_OPTIONS = new Set(['env', 'maxBuffer']);
const DEEP_OPTIONS = new Set(['env', ...FD_SPECIFIC_OPTIONS]);
4 changes: 2 additions & 2 deletions lib/arguments/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {validateEncoding, BINARY_ENCODINGS} from './encoding.js';
import {handleNodeOption} from './node.js';
import {joinCommand} from './escape.js';
import {normalizeCwd, normalizeFileUrl} from './cwd.js';
import {normalizeFdSpecificOptions} from './specific.js';
import {normalizeFdSpecificOptions, normalizeFdSpecificOption} from './specific.js';

export const handleCommand = (filePath, rawArgs, rawOptions) => {
const startTime = getStartTime();
const {command, escapedCommand} = joinCommand(filePath, rawArgs);
const verboseInfo = getVerboseInfo(rawOptions);
const verboseInfo = getVerboseInfo(normalizeFdSpecificOption(rawOptions, 'verbose'));
logCommand(escapedCommand, verboseInfo, rawOptions);
return {command, escapedCommand, startTime, verboseInfo};
};
Expand Down
20 changes: 13 additions & 7 deletions lib/arguments/specific.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import isPlainObject from 'is-plain-obj';
import {STANDARD_STREAMS_ALIASES} from '../utils.js';
import {verboseDefault} from '../verbose/info.js';

export const normalizeFdSpecificOptions = options => {
const optionBaseArray = Array.from({length: getStdioLength(options)});

const optionsCopy = {...options};

for (const optionName of FD_SPECIFIC_OPTIONS) {
const optionArray = normalizeFdSpecificOption(options[optionName], [...optionBaseArray], optionName);
optionsCopy[optionName] = addDefaultValue(optionArray, optionName);
optionsCopy[optionName] = normalizeFdSpecificOption(options, optionName);
}

return optionsCopy;
};

export const normalizeFdSpecificOption = (options, optionName) => {
const optionBaseArray = Array.from({length: getStdioLength(options)});
const optionArray = normalizeFdSpecificValue(options[optionName], optionBaseArray, optionName);
return addDefaultValue(optionArray, optionName);
};

const getStdioLength = ({stdio}) => Array.isArray(stdio)
? Math.max(stdio.length, STANDARD_STREAMS_ALIASES.length)
: STANDARD_STREAMS_ALIASES.length;

const FD_SPECIFIC_OPTIONS = ['maxBuffer'];

const normalizeFdSpecificOption = (optionValue, optionArray, optionName) => isPlainObject(optionValue)
const normalizeFdSpecificValue = (optionValue, optionArray, optionName) => isPlainObject(optionValue)
? normalizeOptionObject(optionValue, optionArray, optionName)
: optionArray.fill(optionValue);

Expand Down Expand Up @@ -71,4 +74,7 @@ const addDefaultValue = (optionArray, optionName) => optionArray.map(optionValue

const DEFAULT_OPTIONS = {
maxBuffer: 1000 * 1000 * 100,
verbose: verboseDefault,
};

export const FD_SPECIFIC_OPTIONS = ['maxBuffer', 'verbose'];
4 changes: 2 additions & 2 deletions lib/stdio/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const addDefaultValue = (stdioOption, fdNumber) => {
return stdioOption;
};

const normalizeStdioSync = (stdioArray, buffer, verbose) => buffer || verbose === 'full'
const normalizeStdioSync = (stdioArray, buffer, verbose) => buffer
? stdioArray
: stdioArray.map((stdioOption, fdNumber) => fdNumber !== 0 && isOutputPipeOnly(stdioOption) ? 'ignore' : stdioOption);
: stdioArray.map((stdioOption, fdNumber) => fdNumber !== 0 && verbose[fdNumber] !== 'full' && isOutputPipeOnly(stdioOption) ? 'ignore' : stdioOption);

const isOutputPipeOnly = stdioOption => stdioOption === 'pipe'
|| (Array.isArray(stdioOption) && stdioOption.every(item => item === 'pipe'));
Expand Down
3 changes: 2 additions & 1 deletion lib/verbose/complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import prettyMs from 'pretty-ms';
import {gray} from 'yoctocolors';
import {escapeLines} from '../arguments/escape.js';
import {getDurationMs} from '../return/duration.js';
import {isVerbose} from './info.js';
import {verboseLog} from './log.js';
import {logError} from './error.js';

Expand All @@ -22,7 +23,7 @@ export const logEarlyResult = (error, startTime, verboseInfo) => {
};

const logResult = ({message, failed, reject, durationMs, verboseInfo: {verbose, verboseId}}) => {
if (verbose === 'none') {
if (!isVerbose(verbose)) {
return;
}

Expand Down
11 changes: 7 additions & 4 deletions lib/verbose/info.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {debuglog} from 'node:util';

export const getVerboseInfo = ({verbose = verboseDefault}) => verbose === 'none'
? {verbose}
: {verbose, verboseId: VERBOSE_ID++};
export const verboseDefault = debuglog('execa').enabled ? 'full' : 'none';

const verboseDefault = debuglog('execa').enabled ? 'full' : 'none';
export const getVerboseInfo = verbose => {
const verboseId = isVerbose(verbose) ? VERBOSE_ID++ : undefined;
return {verbose, verboseId};
};

// Prepending the `pid` is useful when multiple commands print their output at the same time.
// However, we cannot use the real PID since this is not available with `child_process.spawnSync()`.
// Also, we cannot use the real PID if we want to print it before `child_process.spawn()` is run.
// As a pro, it is shorter than a normal PID and never re-uses the same id.
// As a con, it cannot be used to send signals.
let VERBOSE_ID = 0n;

export const isVerbose = verbose => verbose.some(fdVerbose => fdVerbose !== 'none');
2 changes: 1 addition & 1 deletion lib/verbose/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {verboseLog} from './log.js';
// `inherit` would result in double printing.
// They can also lead to double printing when passing file descriptor integers or `process.std*`.
// This only leaves with `pipe` and `overlapped`.
export const shouldLogOutput = ({stdioItems, encoding, verboseInfo: {verbose}, fdNumber}) => verbose === 'full'
export const shouldLogOutput = ({stdioItems, encoding, verboseInfo: {verbose}, fdNumber}) => verbose[fdNumber] === 'full'
&& !BINARY_ENCODINGS.has(encoding)
&& fdUsesVerbose(fdNumber)
&& (stdioItems.some(({type, value}) => type === 'native' && PIPED_STDIO_VALUES.has(value))
Expand Down
3 changes: 2 additions & 1 deletion lib/verbose/start.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {bold} from 'yoctocolors';
import {isVerbose} from './info.js';
import {verboseLog} from './log.js';

// When `verbose` is `short|full`, print each command
export const logCommand = (escapedCommand, {verbose, verboseId}, {piped = false}) => {
if (verbose === 'none') {
if (!isVerbose(verbose)) {
return;
}

Expand Down
8 changes: 5 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,11 +756,11 @@ Type: `object`

This lists all options for [`execa()`](#execafile-arguments-options) and the [other methods](#methods).

Some options are related to the subprocess output: [`maxBuffer`](#optionsmaxbuffer). By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.
Some options are related to the subprocess output: [`verbose`](#optionsverbose), [`maxBuffer`](#optionsmaxbuffer). By default, those options apply to all file descriptors (`stdout`, `stderr`, etc.). A plain object can be passed instead to apply them to only `stdout`, `stderr`, `fd3`, etc.

```js
await execa('./run.js', {maxBuffer: 1e6}) // Same value for stdout and stderr
await execa('./run.js', {maxBuffer: {stdout: 1e4, stderr: 1e6}}) // Different values
await execa('./run.js', {verbose: 'full'}) // Same value for stdout and stderr
await execa('./run.js', {verbose: {stdout: 'none', stderr: 'full'}}) // Different values
```

#### options.reject
Expand Down Expand Up @@ -864,6 +864,8 @@ If `verbose` is `'full'`, the command's `stdout` and `stderr` are printed too, u

This can also be set to `'full'` by setting the `NODE_DEBUG=execa` environment variable in the current process.

By default, this applies to both `stdout` and `stderr`, but different values can also be passed.

#### options.buffer

Type: `boolean`\
Expand Down
15 changes: 13 additions & 2 deletions test/helpers/verbose.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {nestedExecaAsync, nestedExecaSync} from './nested.js';
const isWindows = platform === 'win32';
export const QUOTE = isWindows ? '"' : '\'';

const runErrorSubprocess = async (execaMethod, t, verbose) => {
const runErrorSubprocess = async (execaMethod, t, verbose, expectExitCode = true) => {
const subprocess = execaMethod('noop-fail.js', ['1', foobarString], {verbose});
await t.throwsAsync(subprocess);
const {stderr} = await subprocess.parent;
if (verbose !== 'none') {
if (expectExitCode) {
t.true(stderr.includes('exit code 2'));
}

Expand Down Expand Up @@ -62,3 +62,14 @@ const normalizeTimestamp = stderr => stderr.replaceAll(/^\[\d{2}:\d{2}:\d{2}.\d{
const normalizeDuration = stderr => stderr.replaceAll(/\(done in [^)]+\)/g, '(done in 0ms)');

export const getVerboseOption = (isVerbose, verbose = 'short') => ({verbose: isVerbose ? verbose : 'none'});

export const fdNoneOption = {stdout: 'none', stderr: 'none'};
export const fdShortOption = {stdout: 'short', stderr: 'none'};
export const fdFullOption = {stdout: 'full', stderr: 'none'};
export const fdStdoutNoneOption = {stdout: 'none', stderr: 'full'};
export const fdStderrNoneOption = {stdout: 'full', stderr: 'none'};
export const fdStderrShortOption = {stdout: 'none', stderr: 'short'};
export const fdStderrFullOption = {stdout: 'none', stderr: 'full'};
export const fd3NoneOption = {stdout: 'full', fd3: 'none'};
export const fd3ShortOption = {stdout: 'none', fd3: 'short'};
export const fd3FullOption = {stdout: 'none', fd3: 'full'};
21 changes: 17 additions & 4 deletions test/verbose/complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
getCompletionLines,
testTimestamp,
getVerboseOption,
fdNoneOption,
fdShortOption,
fdFullOption,
} from '../helpers/verbose.js';

setFixtureDir();
Expand All @@ -26,16 +29,26 @@ const testPrintCompletion = async (t, verbose, execaMethod) => {

test('Prints completion, verbose "short"', testPrintCompletion, 'short', parentExecaAsync);
test('Prints completion, verbose "full"', testPrintCompletion, 'full', parentExecaAsync);
test('Prints completion, verbose "short", fd-specific', testPrintCompletion, fdShortOption, parentExecaAsync);
test('Prints completion, verbose "full", fd-specific', testPrintCompletion, fdFullOption, parentExecaAsync);
test('Prints completion, verbose "short", sync', testPrintCompletion, 'short', parentExecaSync);
test('Prints completion, verbose "full", sync', testPrintCompletion, 'full', parentExecaSync);
test('Prints completion, verbose "short", fd-specific, sync', testPrintCompletion, fdShortOption, parentExecaSync);
test('Prints completion, verbose "full", fd-specific, sync', testPrintCompletion, fdFullOption, parentExecaSync);

const testNoPrintCompletion = async (t, execaMethod) => {
const {stderr} = await execaMethod('noop.js', [foobarString], {verbose: 'none'});
const testNoPrintCompletion = async (t, verbose, execaMethod) => {
const {stderr} = await execaMethod('noop.js', [foobarString], {verbose});
t.is(stderr, '');
};

test('Does not print completion, verbose "none"', testNoPrintCompletion, parentExecaAsync);
test('Does not print completion, verbose "none", sync', testNoPrintCompletion, parentExecaSync);
test('Does not print completion, verbose "none"', testNoPrintCompletion, 'none', parentExecaAsync);
test('Does not print completion, verbose default"', testNoPrintCompletion, undefined, parentExecaAsync);
test('Does not print completion, verbose "none", fd-specific', testNoPrintCompletion, fdNoneOption, parentExecaAsync);
test('Does not print completion, verbose default", fd-specific', testNoPrintCompletion, {}, parentExecaAsync);
test('Does not print completion, verbose "none", sync', testNoPrintCompletion, 'none', parentExecaSync);
test('Does not print completion, verbose default", sync', testNoPrintCompletion, undefined, parentExecaSync);
test('Does not print completion, verbose "none", fd-specific, sync', testNoPrintCompletion, fdNoneOption, parentExecaSync);
test('Does not print completion, verbose default", fd-specific, sync', testNoPrintCompletion, {}, parentExecaSync);

const testPrintCompletionError = async (t, execaMethod) => {
const stderr = await execaMethod(t, 'short');
Expand Down
21 changes: 17 additions & 4 deletions test/verbose/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
getErrorLines,
testTimestamp,
getVerboseOption,
fdNoneOption,
fdShortOption,
fdFullOption,
} from '../helpers/verbose.js';

setFixtureDir();
Expand All @@ -27,16 +30,26 @@ const testPrintError = async (t, verbose, execaMethod) => {

test('Prints error, verbose "short"', testPrintError, 'short', runErrorSubprocessAsync);
test('Prints error, verbose "full"', testPrintError, 'full', runErrorSubprocessAsync);
test('Prints error, verbose "short", fd-specific', testPrintError, fdShortOption, runErrorSubprocessAsync);
test('Prints error, verbose "full", fd-specific', testPrintError, fdFullOption, runErrorSubprocessAsync);
test('Prints error, verbose "short", sync', testPrintError, 'short', runErrorSubprocessSync);
test('Prints error, verbose "full", sync', testPrintError, 'full', runErrorSubprocessSync);
test('Prints error, verbose "short", fd-specific, sync', testPrintError, fdShortOption, runErrorSubprocessSync);
test('Prints error, verbose "full", fd-specific, sync', testPrintError, fdFullOption, runErrorSubprocessSync);

const testNoPrintError = async (t, execaMethod) => {
const stderr = await execaMethod(t, 'none');
const testNoPrintError = async (t, verbose, execaMethod) => {
const stderr = await execaMethod(t, verbose, false);
t.is(getErrorLine(stderr), undefined);
};

test('Does not print error, verbose "none"', testNoPrintError, runErrorSubprocessAsync);
test('Does not print error, verbose "none", sync', testNoPrintError, runErrorSubprocessSync);
test('Does not print error, verbose "none"', testNoPrintError, 'none', runErrorSubprocessAsync);
test('Does not print error, verbose default', testNoPrintError, undefined, runErrorSubprocessAsync);
test('Does not print error, verbose "none", fd-specific', testNoPrintError, fdNoneOption, runErrorSubprocessAsync);
test('Does not print error, verbose default, fd-specific', testNoPrintError, {}, runErrorSubprocessAsync);
test('Does not print error, verbose "none", sync', testNoPrintError, 'none', runErrorSubprocessSync);
test('Does not print error, verbose default, sync', testNoPrintError, undefined, runErrorSubprocessSync);
test('Does not print error, verbose "none", fd-specific, sync', testNoPrintError, fdNoneOption, runErrorSubprocessSync);
test('Does not print error, verbose default, fd-specific, sync', testNoPrintError, {}, runErrorSubprocessSync);

const testPrintNoError = async (t, execaMethod) => {
const {stderr} = await execaMethod('noop.js', [foobarString], {verbose: 'short'});
Expand Down

0 comments on commit d59788a

Please sign in to comment.