UNPKG

webdriverio-automation

Version:

WebdriverIO-Automation android ios project

138 lines (124 loc) 4.43 kB
/* 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;