180 lines
6.3 KiB
JavaScript
180 lines
6.3 KiB
JavaScript
'use strict';
|
|
|
|
// Generic `child_process` runner that handles running the given module with the
|
|
// given `runArguments` and returning the async result. Handles the complexity
|
|
// error handling, passing large argument objects, and timeouts.
|
|
//
|
|
// Error handling includes main-line errors seen while waiting the async result,
|
|
// as well as keeping track of out of band `uncaughtException` and
|
|
// `unhandledRejection` to give more context if the process exits with code 1
|
|
// (error) or timesout.
|
|
|
|
const assert = require('assert');
|
|
const { fork } = require('child_process');
|
|
|
|
const RethrownError = require('../lib/rethrown-error');
|
|
const { traceFunction } = require('../tracing/trace-utilities');
|
|
|
|
const config = require('../lib/config');
|
|
const logOutputFromChildProcesses = config.get('logOutputFromChildProcesses');
|
|
|
|
if (!logOutputFromChildProcesses) {
|
|
console.warn(
|
|
`Silencing logs from child processes (config.logOutputFromChildProcesses = ${logOutputFromChildProcesses})`
|
|
);
|
|
}
|
|
|
|
function assembleErrorAfterChildExitsWithErrors(exitCode, childErrors) {
|
|
assert(childErrors);
|
|
|
|
let extraErrorsMessage = '';
|
|
if (childErrors.length > 1) {
|
|
extraErrorsMessage = ` (somehow we saw ${
|
|
childErrors.length
|
|
} errors but we really always expect 1 error)\n${childErrors
|
|
.map((childError, index) => `${index}. ${childError.stack}`)
|
|
.join('\n')}`;
|
|
}
|
|
|
|
let childErrorToDisplay;
|
|
if (childErrors.length === 0) {
|
|
childErrorToDisplay = new Error('No child errors');
|
|
// Clear the stack trace part of the stack string out because this is just a
|
|
// note about the lack of errors, not an actual error and is just noisy with
|
|
// that extra fluff.
|
|
childErrorToDisplay.stack = childErrorToDisplay.message;
|
|
} else if (childErrors.length === 1) {
|
|
childErrorToDisplay = childErrors[0];
|
|
} else {
|
|
childErrorToDisplay = new Error('Multiple child errors listed above ^');
|
|
// Clear the stack trace part of the stack string out because this is just a
|
|
// note about the other errors, not an actual error and is just noisy with
|
|
// that extra fluff.
|
|
childErrorToDisplay.stack = childErrorToDisplay.message;
|
|
}
|
|
|
|
const childErrorSummary = new RethrownError(
|
|
`Child process exited with code ${exitCode}${extraErrorsMessage}`,
|
|
childErrorToDisplay
|
|
);
|
|
|
|
return childErrorSummary;
|
|
}
|
|
|
|
async function runInChildProcess(modulePath, runArguments, { timeout }) {
|
|
let abortTimeoutId;
|
|
try {
|
|
let childErrors = [];
|
|
let childExitCode = '(not set yet)';
|
|
|
|
const controller = new AbortController();
|
|
const { signal } = controller;
|
|
// We use a child_process because we want to be able to exit the process
|
|
// after we receive the results. We use `fork` instead of `exec`/`spawn` so
|
|
// that we can pass a module instead of running a command.
|
|
const child = fork(require.resolve('./child-fork-script'), [modulePath], {
|
|
signal,
|
|
// Default to silencing logs from the child process. We already have
|
|
// proper instrumentation of any errors that might occur.
|
|
//
|
|
// This also makes `child.stderr` and `child.stdout` available
|
|
silent: true,
|
|
//cwd: process.cwd(),
|
|
});
|
|
|
|
// Since we have to use the `silent` option for the `stderr` stuff below, we
|
|
// should also print out the `stdout` to our main console.
|
|
if (logOutputFromChildProcesses) {
|
|
child.stdout.on('data', function (data) {
|
|
console.log('Child printed something to stdout:', String(data));
|
|
});
|
|
|
|
child.stderr.on('data', function (data) {
|
|
console.log('Child printed something to stderr:', String(data));
|
|
});
|
|
}
|
|
|
|
// Pass the runArguments to the child by sending instead of via argv because
|
|
// we will run into `Error: spawn E2BIG` and `Error: spawn ENAMETOOLONG`
|
|
// with argv.
|
|
child.send(runArguments);
|
|
|
|
// Stops the child process if it takes too long
|
|
if (timeout) {
|
|
abortTimeoutId = setTimeout(() => {
|
|
controller.abort();
|
|
}, timeout);
|
|
}
|
|
|
|
const returnedData = await new Promise((resolve, reject) => {
|
|
let data = '';
|
|
// Collect the data passed back by the child
|
|
child.on('message', function (result) {
|
|
if (result.error) {
|
|
// De-serialize the error
|
|
const childError = new Error();
|
|
childError.name = result.name;
|
|
childError.message = result.message;
|
|
childError.stack = result.stack;
|
|
// When an error happens while running the module, we only expect one
|
|
// error to come through here from the main-line to run the module.
|
|
// But it's possible to get multiple errors from async out of context
|
|
// places since we also listen to `uncaughtException` and
|
|
// `unhandledRejection`.
|
|
childErrors.push(childError);
|
|
} else {
|
|
data += result.data;
|
|
}
|
|
});
|
|
|
|
child.on('close', (exitCode) => {
|
|
childExitCode = exitCode;
|
|
// Exited successfully
|
|
if (exitCode === 0) {
|
|
resolve(data);
|
|
} else {
|
|
const childErrorSummary = assembleErrorAfterChildExitsWithErrors(
|
|
childExitCode,
|
|
childErrors
|
|
);
|
|
reject(childErrorSummary);
|
|
}
|
|
});
|
|
|
|
// When a problem occurs when spawning the process or gets aborted
|
|
child.on('error', (err) => {
|
|
if (err.name === 'AbortError') {
|
|
const childErrorSummary = assembleErrorAfterChildExitsWithErrors(
|
|
childExitCode,
|
|
childErrors
|
|
);
|
|
reject(
|
|
new RethrownError(
|
|
`Timed out while running ${modulePath} so we aborted the child process after ${timeout}ms. Any child errors? (${childErrors.length})`,
|
|
childErrorSummary
|
|
)
|
|
);
|
|
} else {
|
|
reject(err);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (!returnedData) {
|
|
const childErrorSummary = assembleErrorAfterChildExitsWithErrors(childExitCode, childErrors);
|
|
throw new RethrownError(
|
|
`No \`returnedData\` sent from child process while running the module (${modulePath}). Any child errors? (${childErrors.length})`,
|
|
childErrorSummary
|
|
);
|
|
}
|
|
|
|
return returnedData;
|
|
} finally {
|
|
// We don't have to add a undefined/null check here because `clearTimeout`
|
|
// works with any value you give it and doesn't throw an error.
|
|
clearTimeout(abortTimeoutId);
|
|
}
|
|
}
|
|
|
|
module.exports = traceFunction(runInChildProcess);
|