Skip to content

Commit

Permalink
Refactor verbose logic (#1126)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored Jun 18, 2024
1 parent cbe805c commit f9f1199
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 157 deletions.
7 changes: 4 additions & 3 deletions lib/arguments/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {joinCommand} from './escape.js';
import {normalizeFdSpecificOption} from './specific.js';

// Compute `result.command`, `result.escapedCommand` and `verbose`-related information
export const handleCommand = (filePath, rawArguments, rawOptions) => {
export const handleCommand = (filePath, rawArguments, {piped, ...rawOptions}) => {
const startTime = getStartTime();
const {command, escapedCommand} = joinCommand(filePath, rawArguments);
const verboseInfo = getVerboseInfo(normalizeFdSpecificOption(rawOptions, 'verbose'));
logCommand(escapedCommand, verboseInfo, rawOptions);
const verbose = normalizeFdSpecificOption(rawOptions, 'verbose');
const verboseInfo = getVerboseInfo(verbose, escapedCommand, {...rawOptions});
logCommand(escapedCommand, verboseInfo, piped);
return {
command,
escapedCommand,
Expand Down
10 changes: 9 additions & 1 deletion lib/arguments/specific.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {debuglog} from 'node:util';
import isPlainObject from 'is-plain-obj';
import {STANDARD_STREAMS_ALIASES} from '../utils/standard-stream.js';
import {verboseDefault} from '../verbose/info.js';

// Some options can have different values for `stdout`/`stderr`/`fd3`.
// This normalizes those to array of values.
Expand Down Expand Up @@ -91,6 +91,9 @@ const addDefaultValue = (optionArray, optionName) => optionArray.map(optionValue
? DEFAULT_OPTIONS[optionName]
: optionValue);

// Default value for the `verbose` option
const verboseDefault = debuglog('execa').enabled ? 'full' : 'none';

const DEFAULT_OPTIONS = {
lines: false,
buffer: true,
Expand All @@ -101,3 +104,8 @@ const DEFAULT_OPTIONS = {

// List of options which can have different values for `stdout`/`stderr`
export const FD_SPECIFIC_OPTIONS = ['lines', 'buffer', 'maxBuffer', 'verbose', 'stripFinalNewline'];

// Retrieve fd-specific option
export const getFdSpecificValue = (optionArray, fdNumber) => fdNumber === 'ipc'
? optionArray.at(-1)
: optionArray[fdNumber];
60 changes: 39 additions & 21 deletions lib/io/contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,19 @@ import {handleMaxBuffer} from './max-buffer.js';
import {getStripFinalNewline} from './strip-newline.js';

// Retrieve `result.stdout|stderr|all|stdio[*]`
export const getStreamOutput = async ({stream, onStreamEnd, fdNumber, encoding, buffer, maxBuffer, lines, allMixed, stripFinalNewline, verboseInfo, streamInfo: {fileDescriptors}}) => {
if (shouldLogOutput({
stdioItems: fileDescriptors[fdNumber]?.stdioItems,
export const getStreamOutput = async ({stream, onStreamEnd, fdNumber, encoding, buffer, maxBuffer, lines, allMixed, stripFinalNewline, verboseInfo, streamInfo}) => {
const logPromise = logOutputAsync({
stream,
onStreamEnd,
fdNumber,
encoding,
allMixed,
verboseInfo,
fdNumber,
})) {
const linesIterable = iterateForResult({
stream,
onStreamEnd,
lines: true,
encoding,
stripFinalNewline: true,
allMixed,
});
logLines(linesIterable, stream, verboseInfo);
}
streamInfo,
});

if (!buffer) {
await resumeStream(stream);
await Promise.all([resumeStream(stream), logPromise]);
return;
}

Expand All @@ -39,14 +32,39 @@ export const getStreamOutput = async ({stream, onStreamEnd, fdNumber, encoding,
stripFinalNewline: stripFinalNewlineValue,
allMixed,
});
return getStreamContents({
stream,
iterable,
const [output] = await Promise.all([
getStreamContents({
stream,
iterable,
fdNumber,
encoding,
maxBuffer,
lines,
}),
logPromise,
]);
return output;
};

const logOutputAsync = async ({stream, onStreamEnd, fdNumber, encoding, allMixed, verboseInfo, streamInfo: {fileDescriptors}}) => {
if (!shouldLogOutput({
stdioItems: fileDescriptors[fdNumber]?.stdioItems,
encoding,
verboseInfo,
fdNumber,
})) {
return;
}

const linesIterable = iterateForResult({
stream,
onStreamEnd,
lines: true,
encoding,
maxBuffer,
lines,
stripFinalNewline: true,
allMixed,
});
await logLines(linesIterable, stream, verboseInfo);
};

// When using `buffer: false`, users need to read `subprocess.stdout|stderr|all` right away
Expand Down
6 changes: 4 additions & 2 deletions lib/io/max-buffer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {MaxBufferError} from 'get-stream';
import {getStreamName} from '../utils/standard-stream.js';
import {getFdSpecificValue} from '../arguments/specific.js';

// When the `maxBuffer` option is hit, a MaxBufferError is thrown.
// The stream is aborted, then specific information is kept for the error message.
Expand Down Expand Up @@ -59,11 +60,12 @@ const getMaxBufferInfo = (error, maxBuffer) => {
const {maxBufferInfo: {fdNumber, unit}} = error;
delete error.maxBufferInfo;

const threshold = getFdSpecificValue(maxBuffer, fdNumber);
if (fdNumber === 'ipc') {
return {streamName: 'IPC output', threshold: maxBuffer.at(-1), unit: 'messages'};
return {streamName: 'IPC output', threshold, unit: 'messages'};
}

return {streamName: getStreamName(fdNumber), threshold: maxBuffer[fdNumber], unit};
return {streamName: getStreamName(fdNumber), threshold, unit};
};

// The only way to apply `maxBuffer` with `spawnSync()` is to use the native `maxBuffer` option Node.js provides.
Expand Down
29 changes: 21 additions & 8 deletions lib/io/output-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,14 @@ const transformOutputResultSync = (
fdNumber,
});

if (shouldLogOutput({
stdioItems,
encoding,
verboseInfo,
logOutputSync({
serializedResult,
fdNumber,
})) {
const linesArray = splitLinesSync(serializedResult, false, objectMode);
logLinesSync(linesArray, verboseInfo);
}
verboseInfo,
encoding,
stdioItems,
objectMode,
});

const returnedResult = buffer[fdNumber] ? finalResult : undefined;

Expand Down Expand Up @@ -102,6 +101,20 @@ const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline
return {serializedResult};
};

const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdioItems, objectMode}) => {
if (!shouldLogOutput({
stdioItems,
encoding,
verboseInfo,
fdNumber,
})) {
return;
}

const linesArray = splitLinesSync(serializedResult, false, objectMode);
logLinesSync(linesArray, verboseInfo);
};

// When the `std*` target is a file path/URL or a file descriptor
const writeToFiles = (serializedResult, stdioItems, outputFiles) => {
for (const {path} of stdioItems.filter(({type}) => FILE_TYPES.has(type))) {
Expand Down
5 changes: 3 additions & 2 deletions lib/ipc/buffer-messages.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {checkIpcMaxBuffer} from '../io/max-buffer.js';
import {shouldLogIpc, logIpcOutput} from '../verbose/ipc.js';
import {getFdSpecificValue} from '../arguments/specific.js';
import {loopOnMessages} from './get-each.js';

// Iterate through IPC messages sent by the subprocess
Expand All @@ -16,8 +17,8 @@ export const waitForIpcOutput = async ({
}

const isVerbose = shouldLogIpc(verboseInfo);
const buffer = bufferArray.at(-1);
const maxBuffer = maxBufferArray.at(-1);
const buffer = getFdSpecificValue(bufferArray, 'ipc');
const maxBuffer = getFdSpecificValue(maxBufferArray, 'ipc');

for await (const message of loopOnMessages({
anyProcess: subprocess,
Expand Down
3 changes: 2 additions & 1 deletion lib/ipc/outgoing.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {createDeferred} from '../utils/deferred.js';
import {getFdSpecificValue} from '../arguments/specific.js';
import {SUBPROCESS_OPTIONS} from '../arguments/fd-options.js';
import {validateStrictDeadlock} from './strict.js';

Expand Down Expand Up @@ -41,6 +42,6 @@ export const hasMessageListeners = (anyProcess, ipcEmitter) => ipcEmitter.listen
// When `buffer` is `false`, we set up a `message` listener that should be ignored.
// That listener is only meant to intercept `strict` acknowledgement responses.
const getMinListenerCount = anyProcess => SUBPROCESS_OPTIONS.has(anyProcess)
&& !SUBPROCESS_OPTIONS.get(anyProcess).options.buffer.at(-1)
&& !getFdSpecificValue(SUBPROCESS_OPTIONS.get(anyProcess).options.buffer, 'ipc')
? 1
: 0;
2 changes: 1 addition & 1 deletion lib/return/reject.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {logFinalResult} from '../verbose/complete.js';
// Applies the `reject` option.
// Also print the final log line with `verbose`.
export const handleResult = (result, verboseInfo, {reject}) => {
logFinalResult(result, reject, verboseInfo);
logFinalResult(result, verboseInfo);

if (result.failed && reject) {
throw result;
Expand Down
2 changes: 1 addition & 1 deletion lib/stdio/handle.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {filterDuplicates, getDuplicateStream} from './duplicate.js';
// They are converted into an array of `fileDescriptors`.
// Each `fileDescriptor` is normalized, validated and contains all information necessary for further handling.
export const handleStdio = (addProperties, options, verboseInfo, isSync) => {
const stdio = normalizeStdioOption(options, isSync);
const stdio = normalizeStdioOption(options, verboseInfo, isSync);
const initialFileDescriptors = stdio.map((stdioOption, fdNumber) => getFileDescriptor({
stdioOption,
fdNumber,
Expand Down
9 changes: 5 additions & 4 deletions lib/stdio/stdio-option.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {STANDARD_STREAMS_ALIASES} from '../utils/standard-stream.js';
import {normalizeIpcStdioArray} from '../ipc/array.js';
import {isFullVerbose} from '../verbose/info.js';

// Add support for `stdin`/`stdout`/`stderr` as an alias for `stdio`.
// Also normalize the `stdio` option.
export const normalizeStdioOption = ({stdio, ipc, buffer, verbose, ...options}, isSync) => {
export const normalizeStdioOption = ({stdio, ipc, buffer, ...options}, verboseInfo, isSync) => {
const stdioArray = getStdioArray(stdio, options).map((stdioOption, fdNumber) => addDefaultValue(stdioOption, fdNumber));
return isSync
? normalizeStdioSync(stdioArray, buffer, verbose)
? normalizeStdioSync(stdioArray, buffer, verboseInfo)
: normalizeIpcStdioArray(stdioArray, ipc);
};

Expand Down Expand Up @@ -47,10 +48,10 @@ const addDefaultValue = (stdioOption, fdNumber) => {

// Using `buffer: false` with synchronous methods implies `stdout`/`stderr`: `ignore`.
// Unless the output is needed, e.g. due to `verbose: 'full'` or to redirecting to a file.
const normalizeStdioSync = (stdioArray, buffer, verbose) => stdioArray.map((stdioOption, fdNumber) =>
const normalizeStdioSync = (stdioArray, buffer, verboseInfo) => stdioArray.map((stdioOption, fdNumber) =>
!buffer[fdNumber]
&& fdNumber !== 0
&& verbose[fdNumber] !== 'full'
&& !isFullVerbose(verboseInfo, fdNumber)
&& isOutputPipeOnly(stdioOption)
? 'ignore'
: stdioOption);
Expand Down
58 changes: 19 additions & 39 deletions lib/verbose/complete.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,38 @@
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';

// When `verbose` is `short|full`, print each command's completion, duration and error
export const logFinalResult = ({shortMessage, failed, durationMs}, reject, verboseInfo) => {
logResult({
message: shortMessage,
failed,
reject,
durationMs,
verboseInfo,
});
export const logFinalResult = ({shortMessage, durationMs, failed}, verboseInfo) => {
logResult(shortMessage, durationMs, verboseInfo, failed);
};

// Same but for early validation errors
export const logEarlyResult = (error, startTime, verboseInfo) => {
logResult({
message: escapeLines(String(error)),
failed: true,
reject: true,
durationMs: getDurationMs(startTime),
verboseInfo,
});
export const logEarlyResult = (error, startTime, {rawOptions, ...verboseInfo}) => {
const shortMessage = escapeLines(String(error));
const durationMs = getDurationMs(startTime);
const earlyVerboseInfo = {...verboseInfo, rawOptions: {...rawOptions, reject: true}};
logResult(shortMessage, durationMs, earlyVerboseInfo, true);
};

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

const icon = getIcon(failed, reject);
logError({
message,
failed,
reject,
verboseId,
icon,
});
logDuration(durationMs, verboseId, icon);
};

const logDuration = (durationMs, verboseId, icon) => {
const durationMessage = `(done in ${prettyMs(durationMs)})`;
verboseLog(durationMessage, verboseId, icon, gray);
logError(shortMessage, verboseInfo, failed);
logDuration(durationMs, verboseInfo, failed);
};

const getIcon = (failed, reject) => {
if (!failed) {
return 'success';
}

return reject ? 'error' : 'warning';
const logDuration = (durationMs, verboseInfo, failed) => {
const logMessage = `(done in ${prettyMs(durationMs)})`;
verboseLog({
type: 'duration',
logMessage,
verboseInfo,
failed,
});
};
46 changes: 46 additions & 0 deletions lib/verbose/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import figures from 'figures';
import {
gray,
bold,
redBright,
yellowBright,
} from 'yoctocolors';

// Default logger for the `verbose` option
export const defaultLogger = ({type, message, timestamp, failed, piped, commandId, options: {reject = true}}) => {
const timestampString = serializeTimestamp(timestamp);
const icon = ICONS[type]({failed, reject, piped});
const color = COLORS[type]({reject});
return `${gray(`[${timestampString}]`)} ${gray(`[${commandId}]`)} ${color(icon)} ${color(message)}`;
};

// Prepending the timestamp allows debugging the slow paths of a subprocess
const serializeTimestamp = timestamp => `${padField(timestamp.getHours(), 2)}:${padField(timestamp.getMinutes(), 2)}:${padField(timestamp.getSeconds(), 2)}.${padField(timestamp.getMilliseconds(), 3)}`;

const padField = (field, padding) => String(field).padStart(padding, '0');

const getFinalIcon = ({failed, reject}) => {
if (!failed) {
return figures.tick;
}

return reject ? figures.cross : figures.warning;
};

const ICONS = {
command: ({piped}) => piped ? '|' : '$',
output: () => ' ',
ipc: () => '*',
error: getFinalIcon,
duration: getFinalIcon,
};

const identity = string => string;

const COLORS = {
command: () => bold,
output: () => identity,
ipc: () => identity,
error: ({reject}) => reject ? redBright : yellowBright,
duration: () => gray,
};
Loading

0 comments on commit f9f1199

Please sign in to comment.