2023-02-10 20:34:01 +01:00
|
|
|
const fs = require('fs');
|
2021-05-19 19:24:45 +02:00
|
|
|
const spawn = require('cross-spawn');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run a command to completion using the system shell.
|
|
|
|
*
|
|
|
|
* This will run a command with the specified arguments, and resolve when the
|
|
|
|
* process has exited. The STDOUT stream is monitored for output, which is
|
|
|
|
* returned after being split into lines. All output is expected to be UTF-8
|
|
|
|
* encoded, and empty lines are removed from the output.
|
|
|
|
*
|
|
|
|
* Anything received on STDERR is assumed to indicate a problem, and is tracked
|
|
|
|
* as an error.
|
|
|
|
*
|
|
|
|
* @param {string} command - The command to run
|
|
|
|
* @param {Array<string>} [args] - The arguments to pass to the command
|
|
|
|
* @returns {Array<string>} Lines of output received via STDOUT
|
|
|
|
*/
|
|
|
|
async function runCommand(command, args) {
|
|
|
|
const output = [];
|
|
|
|
let mostRecentError;
|
|
|
|
let errorSignal;
|
|
|
|
let errorCode;
|
|
|
|
const internalError = new Error('Internal');
|
|
|
|
try {
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
const childProcess = spawn(command, args, { encoding: 'utf8' });
|
|
|
|
childProcess.stdout.setEncoding('utf8');
|
|
|
|
childProcess.stderr.setEncoding('utf8');
|
|
|
|
|
|
|
|
childProcess.on('error', (error) => {
|
|
|
|
mostRecentError = error;
|
|
|
|
});
|
|
|
|
|
|
|
|
childProcess.stdout.on('data', (message) => {
|
|
|
|
const nonEmptyLines = message.split('\n').filter((line) => line !== '');
|
|
|
|
output.push(...nonEmptyLines);
|
|
|
|
});
|
|
|
|
|
|
|
|
childProcess.stderr.on('data', (message) => {
|
|
|
|
mostRecentError = new Error(message.trim());
|
|
|
|
});
|
|
|
|
|
|
|
|
childProcess.once('exit', (code, signal) => {
|
|
|
|
if (code === 0) {
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
errorCode = code;
|
|
|
|
errorSignal = signal;
|
|
|
|
return reject(internalError);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
/**
|
|
|
|
* The error is re-thrown here in an `async` context to preserve the stack trace. If this was
|
|
|
|
* was thrown inside the Promise constructor, the stack trace would show a few frames of
|
|
|
|
* Node.js internals then end, without indicating where `runCommand` was called.
|
|
|
|
*/
|
|
|
|
if (error === internalError) {
|
|
|
|
let errorMessage;
|
|
|
|
if (errorCode !== null && errorSignal !== null) {
|
|
|
|
errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`;
|
|
|
|
} else if (errorSignal !== null) {
|
|
|
|
errorMessage = `Terminaled by signal '${errorSignal}'`;
|
|
|
|
} else if (errorCode === null) {
|
|
|
|
errorMessage = 'Exited with no code or signal';
|
|
|
|
} else {
|
|
|
|
errorMessage = `Exited with code '${errorCode}'`;
|
|
|
|
}
|
|
|
|
const improvedError = new Error(errorMessage);
|
|
|
|
if (mostRecentError) {
|
|
|
|
improvedError.cause = mostRecentError;
|
|
|
|
}
|
|
|
|
throw improvedError;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run a command to using the system shell.
|
|
|
|
*
|
|
|
|
* This will run a command with the specified arguments, and resolve when the
|
|
|
|
* process has exited. The STDIN, STDOUT and STDERR streams are inherited,
|
|
|
|
* letting the command take over completely until it completes. The success or
|
|
|
|
* failure of the process is determined entirely by the exit code; STDERR
|
|
|
|
* output is not used to indicate failure.
|
|
|
|
*
|
|
|
|
* @param {string} command - The command to run
|
|
|
|
* @param {Array<string>} [args] - The arguments to pass to the command
|
2023-02-10 20:34:01 +01:00
|
|
|
* @param {string} output - The output file to write to
|
2021-05-19 19:24:45 +02:00
|
|
|
*/
|
2023-02-10 20:34:01 +01:00
|
|
|
async function runInShell(command, args, output) {
|
2021-05-19 19:24:45 +02:00
|
|
|
let errorSignal;
|
|
|
|
let errorCode;
|
|
|
|
const internalError = new Error('Internal');
|
|
|
|
try {
|
|
|
|
await new Promise((resolve, reject) => {
|
2023-02-10 20:34:01 +01:00
|
|
|
const childProcess = spawn(command, args);
|
|
|
|
childProcess.stdout.setEncoding('utf8');
|
2023-02-15 08:08:05 +01:00
|
|
|
childProcess.stderr.setEncoding('utf8');
|
2023-02-10 20:34:01 +01:00
|
|
|
childProcess.stdout.pipe(process.stdout);
|
2023-02-15 08:08:05 +01:00
|
|
|
childProcess.stderr.pipe(process.stderr);
|
2023-02-10 20:34:01 +01:00
|
|
|
if (output) {
|
|
|
|
childProcess.stdout.pipe(fs.createWriteStream(output));
|
|
|
|
}
|
2021-05-19 19:24:45 +02:00
|
|
|
|
|
|
|
childProcess.once('exit', (code, signal) => {
|
|
|
|
if (code === 0) {
|
|
|
|
return resolve();
|
|
|
|
}
|
|
|
|
errorCode = code;
|
|
|
|
errorSignal = signal;
|
|
|
|
return reject(internalError);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
/**
|
|
|
|
* The error is re-thrown here in an `async` context to preserve the stack trace. If this was
|
|
|
|
* was thrown inside the Promise constructor, the stack trace would show a few frames of
|
|
|
|
* Node.js internals then end, without indicating where `runInShell` was called.
|
|
|
|
*/
|
|
|
|
if (error === internalError) {
|
|
|
|
let errorMessage;
|
|
|
|
if (errorCode !== null && errorSignal !== null) {
|
|
|
|
errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`;
|
|
|
|
} else if (errorSignal !== null) {
|
|
|
|
errorMessage = `Terminaled by signal '${errorSignal}'`;
|
|
|
|
} else if (errorCode === null) {
|
|
|
|
errorMessage = 'Exited with no code or signal';
|
|
|
|
} else {
|
|
|
|
errorMessage = `Exited with code '${errorCode}'`;
|
|
|
|
}
|
|
|
|
const improvedError = new Error(errorMessage);
|
|
|
|
throw improvedError;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = { runCommand, runInShell };
|