webdriverio-automation
Version:
WebdriverIO-Automation android ios project
138 lines (124 loc) • 4.43 kB
JavaScript
/* eslint-disable promise/prefer-await-to-callbacks */
import { spawn } from 'child_process';
import { quote } from 'shell-quote';
import B from 'bluebird';
import _ from 'lodash';
const MAX_BUFFER_SIZE = 100 * 1024 * 1024;
async function exec (cmd, args = [], opts = {}) {
// get a quoted representation of the command for error strings
const rep = quote([cmd, ...args]);
// extend default options; we're basically re-implementing exec's options
// for use here with spawn under the hood
opts = Object.assign({
timeout: null,
encoding: 'utf8',
killSignal: 'SIGTERM',
cwd: undefined,
env: process.env,
ignoreOutput: false,
stdio: 'inherit',
isBuffer: false,
shell: undefined,
logger: undefined,
maxStdoutBufferSize: MAX_BUFFER_SIZE,
maxStderrBufferSize: MAX_BUFFER_SIZE,
}, opts);
// this is an async function, so return a promise
return await new B((resolve, reject) => {
// spawn the child process with options; we don't currently expose any of
// the other 'spawn' options through the API
let proc = spawn(cmd, args, {cwd: opts.cwd, env: opts.env, shell: opts.shell});
let stdoutArr = [], stderrArr = [], timer = null;
// if the process errors out, reject the promise
proc.on('error', (err) => {
let msg = `Command '${rep}' errored out: ${err.stack}`;
if (err.errno === 'ENOENT') {
msg = `Command '${cmd}' not found. Is it installed?`;
}
reject(new Error(msg));
});
if (proc.stdin) {
proc.stdin.on('error', (err) => {
reject(new Error(`Standard input '${err.syscall}' error: ${err.stack}`));
});
}
const handleStream = (streamType, streamProps) => {
if (!proc[streamType]) {
return;
}
proc[streamType].on('error', (err) => {
reject(new Error(`${_.capitalize(streamType)} '${err.syscall}' error: ${err.stack}`));
});
if (opts.ignoreOutput) {
// https://github.com/nodejs/node/issues/4236
proc[streamType].on('data', () => {});
return;
}
// keep track of the stream if we don't want to ignore it
const {chunks, maxSize} = streamProps;
let size = 0;
proc[streamType].on('data', (chunk) => {
chunks.push(chunk);
size += chunk.length;
while (chunks.length > 1 && size >= maxSize) {
size -= chunks[0].length;
chunks.shift();
}
if (opts.logger && _.isFunction(opts.logger.debug)) {
opts.logger.debug(chunk.toString());
}
});
};
handleStream('stdout', {
maxSize: opts.maxStdoutBufferSize,
chunks: stdoutArr,
});
handleStream('stderr', {
maxSize: opts.maxStderrBufferSize,
chunks: stderrArr,
});
function getStdio (isBuffer) {
let stdout, stderr;
if (isBuffer) {
stdout = Buffer.concat(stdoutArr);
stderr = Buffer.concat(stderrArr);
} else {
stdout = Buffer.concat(stdoutArr).toString(opts.encoding);
stderr = Buffer.concat(stderrArr).toString(opts.encoding);
}
return {stdout, stderr};
}
// if the process ends, either resolve or reject the promise based on the
// exit code of the process. either way, attach stdout, stderr, and code.
// Also clean up the timer if it exists
proc.on('close', (code) => {
if (timer) {
clearTimeout(timer);
}
let {stdout, stderr} = getStdio(opts.isBuffer);
if (code === 0) {
resolve({stdout, stderr, code});
} else {
let err = new Error(`Command '${rep}' exited with code ${code}`);
err = Object.assign(err, {stdout, stderr, code});
reject(err);
}
});
// if we set a timeout on the child process, cut into the execution and
// reject if the timeout is reached. Attach the stdout/stderr we currently
// have in case it's helpful in debugging
if (opts.timeout) {
timer = setTimeout(() => {
let {stdout, stderr} = getStdio(opts.isBuffer);
let err = new Error(`Command '${rep}' timed out after ${opts.timeout}ms`);
err = Object.assign(err, {stdout, stderr, code: null});
reject(err);
// reject and THEN kill to avoid race conditions with the handlers
// above
proc.kill(opts.killSignal);
}, opts.timeout);
}
});
}
export { exec };
export default exec;