[6a3a178] | 1 | 'use strict';
|
---|
| 2 | const path = require('path');
|
---|
| 3 | const childProcess = require('child_process');
|
---|
| 4 | const crossSpawn = require('cross-spawn');
|
---|
| 5 | const stripEof = require('strip-eof');
|
---|
| 6 | const npmRunPath = require('npm-run-path');
|
---|
| 7 | const isStream = require('is-stream');
|
---|
| 8 | const _getStream = require('get-stream');
|
---|
| 9 | const pFinally = require('p-finally');
|
---|
| 10 | const onExit = require('signal-exit');
|
---|
| 11 | const errname = require('./lib/errname');
|
---|
| 12 | const stdio = require('./lib/stdio');
|
---|
| 13 |
|
---|
| 14 | const TEN_MEGABYTES = 1000 * 1000 * 10;
|
---|
| 15 |
|
---|
| 16 | function handleArgs(cmd, args, opts) {
|
---|
| 17 | let parsed;
|
---|
| 18 |
|
---|
| 19 | opts = Object.assign({
|
---|
| 20 | extendEnv: true,
|
---|
| 21 | env: {}
|
---|
| 22 | }, opts);
|
---|
| 23 |
|
---|
| 24 | if (opts.extendEnv) {
|
---|
| 25 | opts.env = Object.assign({}, process.env, opts.env);
|
---|
| 26 | }
|
---|
| 27 |
|
---|
| 28 | if (opts.__winShell === true) {
|
---|
| 29 | delete opts.__winShell;
|
---|
| 30 | parsed = {
|
---|
| 31 | command: cmd,
|
---|
| 32 | args,
|
---|
| 33 | options: opts,
|
---|
| 34 | file: cmd,
|
---|
| 35 | original: {
|
---|
| 36 | cmd,
|
---|
| 37 | args
|
---|
| 38 | }
|
---|
| 39 | };
|
---|
| 40 | } else {
|
---|
| 41 | parsed = crossSpawn._parse(cmd, args, opts);
|
---|
| 42 | }
|
---|
| 43 |
|
---|
| 44 | opts = Object.assign({
|
---|
| 45 | maxBuffer: TEN_MEGABYTES,
|
---|
| 46 | buffer: true,
|
---|
| 47 | stripEof: true,
|
---|
| 48 | preferLocal: true,
|
---|
| 49 | localDir: parsed.options.cwd || process.cwd(),
|
---|
| 50 | encoding: 'utf8',
|
---|
| 51 | reject: true,
|
---|
| 52 | cleanup: true
|
---|
| 53 | }, parsed.options);
|
---|
| 54 |
|
---|
| 55 | opts.stdio = stdio(opts);
|
---|
| 56 |
|
---|
| 57 | if (opts.preferLocal) {
|
---|
| 58 | opts.env = npmRunPath.env(Object.assign({}, opts, {cwd: opts.localDir}));
|
---|
| 59 | }
|
---|
| 60 |
|
---|
| 61 | if (opts.detached) {
|
---|
| 62 | // #115
|
---|
| 63 | opts.cleanup = false;
|
---|
| 64 | }
|
---|
| 65 |
|
---|
| 66 | if (process.platform === 'win32' && path.basename(parsed.command) === 'cmd.exe') {
|
---|
| 67 | // #116
|
---|
| 68 | parsed.args.unshift('/q');
|
---|
| 69 | }
|
---|
| 70 |
|
---|
| 71 | return {
|
---|
| 72 | cmd: parsed.command,
|
---|
| 73 | args: parsed.args,
|
---|
| 74 | opts,
|
---|
| 75 | parsed
|
---|
| 76 | };
|
---|
| 77 | }
|
---|
| 78 |
|
---|
| 79 | function handleInput(spawned, input) {
|
---|
| 80 | if (input === null || input === undefined) {
|
---|
| 81 | return;
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | if (isStream(input)) {
|
---|
| 85 | input.pipe(spawned.stdin);
|
---|
| 86 | } else {
|
---|
| 87 | spawned.stdin.end(input);
|
---|
| 88 | }
|
---|
| 89 | }
|
---|
| 90 |
|
---|
| 91 | function handleOutput(opts, val) {
|
---|
| 92 | if (val && opts.stripEof) {
|
---|
| 93 | val = stripEof(val);
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | return val;
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | function handleShell(fn, cmd, opts) {
|
---|
| 100 | let file = '/bin/sh';
|
---|
| 101 | let args = ['-c', cmd];
|
---|
| 102 |
|
---|
| 103 | opts = Object.assign({}, opts);
|
---|
| 104 |
|
---|
| 105 | if (process.platform === 'win32') {
|
---|
| 106 | opts.__winShell = true;
|
---|
| 107 | file = process.env.comspec || 'cmd.exe';
|
---|
| 108 | args = ['/s', '/c', `"${cmd}"`];
|
---|
| 109 | opts.windowsVerbatimArguments = true;
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | if (opts.shell) {
|
---|
| 113 | file = opts.shell;
|
---|
| 114 | delete opts.shell;
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | return fn(file, args, opts);
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | function getStream(process, stream, {encoding, buffer, maxBuffer}) {
|
---|
| 121 | if (!process[stream]) {
|
---|
| 122 | return null;
|
---|
| 123 | }
|
---|
| 124 |
|
---|
| 125 | let ret;
|
---|
| 126 |
|
---|
| 127 | if (!buffer) {
|
---|
| 128 | // TODO: Use `ret = util.promisify(stream.finished)(process[stream]);` when targeting Node.js 10
|
---|
| 129 | ret = new Promise((resolve, reject) => {
|
---|
| 130 | process[stream]
|
---|
| 131 | .once('end', resolve)
|
---|
| 132 | .once('error', reject);
|
---|
| 133 | });
|
---|
| 134 | } else if (encoding) {
|
---|
| 135 | ret = _getStream(process[stream], {
|
---|
| 136 | encoding,
|
---|
| 137 | maxBuffer
|
---|
| 138 | });
|
---|
| 139 | } else {
|
---|
| 140 | ret = _getStream.buffer(process[stream], {maxBuffer});
|
---|
| 141 | }
|
---|
| 142 |
|
---|
| 143 | return ret.catch(err => {
|
---|
| 144 | err.stream = stream;
|
---|
| 145 | err.message = `${stream} ${err.message}`;
|
---|
| 146 | throw err;
|
---|
| 147 | });
|
---|
| 148 | }
|
---|
| 149 |
|
---|
| 150 | function makeError(result, options) {
|
---|
| 151 | const {stdout, stderr} = result;
|
---|
| 152 |
|
---|
| 153 | let err = result.error;
|
---|
| 154 | const {code, signal} = result;
|
---|
| 155 |
|
---|
| 156 | const {parsed, joinedCmd} = options;
|
---|
| 157 | const timedOut = options.timedOut || false;
|
---|
| 158 |
|
---|
| 159 | if (!err) {
|
---|
| 160 | let output = '';
|
---|
| 161 |
|
---|
| 162 | if (Array.isArray(parsed.opts.stdio)) {
|
---|
| 163 | if (parsed.opts.stdio[2] !== 'inherit') {
|
---|
| 164 | output += output.length > 0 ? stderr : `\n${stderr}`;
|
---|
| 165 | }
|
---|
| 166 |
|
---|
| 167 | if (parsed.opts.stdio[1] !== 'inherit') {
|
---|
| 168 | output += `\n${stdout}`;
|
---|
| 169 | }
|
---|
| 170 | } else if (parsed.opts.stdio !== 'inherit') {
|
---|
| 171 | output = `\n${stderr}${stdout}`;
|
---|
| 172 | }
|
---|
| 173 |
|
---|
| 174 | err = new Error(`Command failed: ${joinedCmd}${output}`);
|
---|
| 175 | err.code = code < 0 ? errname(code) : code;
|
---|
| 176 | }
|
---|
| 177 |
|
---|
| 178 | err.stdout = stdout;
|
---|
| 179 | err.stderr = stderr;
|
---|
| 180 | err.failed = true;
|
---|
| 181 | err.signal = signal || null;
|
---|
| 182 | err.cmd = joinedCmd;
|
---|
| 183 | err.timedOut = timedOut;
|
---|
| 184 |
|
---|
| 185 | return err;
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | function joinCmd(cmd, args) {
|
---|
| 189 | let joinedCmd = cmd;
|
---|
| 190 |
|
---|
| 191 | if (Array.isArray(args) && args.length > 0) {
|
---|
| 192 | joinedCmd += ' ' + args.join(' ');
|
---|
| 193 | }
|
---|
| 194 |
|
---|
| 195 | return joinedCmd;
|
---|
| 196 | }
|
---|
| 197 |
|
---|
| 198 | module.exports = (cmd, args, opts) => {
|
---|
| 199 | const parsed = handleArgs(cmd, args, opts);
|
---|
| 200 | const {encoding, buffer, maxBuffer} = parsed.opts;
|
---|
| 201 | const joinedCmd = joinCmd(cmd, args);
|
---|
| 202 |
|
---|
| 203 | let spawned;
|
---|
| 204 | try {
|
---|
| 205 | spawned = childProcess.spawn(parsed.cmd, parsed.args, parsed.opts);
|
---|
| 206 | } catch (err) {
|
---|
| 207 | return Promise.reject(err);
|
---|
| 208 | }
|
---|
| 209 |
|
---|
| 210 | let removeExitHandler;
|
---|
| 211 | if (parsed.opts.cleanup) {
|
---|
| 212 | removeExitHandler = onExit(() => {
|
---|
| 213 | spawned.kill();
|
---|
| 214 | });
|
---|
| 215 | }
|
---|
| 216 |
|
---|
| 217 | let timeoutId = null;
|
---|
| 218 | let timedOut = false;
|
---|
| 219 |
|
---|
| 220 | const cleanup = () => {
|
---|
| 221 | if (timeoutId) {
|
---|
| 222 | clearTimeout(timeoutId);
|
---|
| 223 | timeoutId = null;
|
---|
| 224 | }
|
---|
| 225 |
|
---|
| 226 | if (removeExitHandler) {
|
---|
| 227 | removeExitHandler();
|
---|
| 228 | }
|
---|
| 229 | };
|
---|
| 230 |
|
---|
| 231 | if (parsed.opts.timeout > 0) {
|
---|
| 232 | timeoutId = setTimeout(() => {
|
---|
| 233 | timeoutId = null;
|
---|
| 234 | timedOut = true;
|
---|
| 235 | spawned.kill(parsed.opts.killSignal);
|
---|
| 236 | }, parsed.opts.timeout);
|
---|
| 237 | }
|
---|
| 238 |
|
---|
| 239 | const processDone = new Promise(resolve => {
|
---|
| 240 | spawned.on('exit', (code, signal) => {
|
---|
| 241 | cleanup();
|
---|
| 242 | resolve({code, signal});
|
---|
| 243 | });
|
---|
| 244 |
|
---|
| 245 | spawned.on('error', err => {
|
---|
| 246 | cleanup();
|
---|
| 247 | resolve({error: err});
|
---|
| 248 | });
|
---|
| 249 |
|
---|
| 250 | if (spawned.stdin) {
|
---|
| 251 | spawned.stdin.on('error', err => {
|
---|
| 252 | cleanup();
|
---|
| 253 | resolve({error: err});
|
---|
| 254 | });
|
---|
| 255 | }
|
---|
| 256 | });
|
---|
| 257 |
|
---|
| 258 | function destroy() {
|
---|
| 259 | if (spawned.stdout) {
|
---|
| 260 | spawned.stdout.destroy();
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | if (spawned.stderr) {
|
---|
| 264 | spawned.stderr.destroy();
|
---|
| 265 | }
|
---|
| 266 | }
|
---|
| 267 |
|
---|
| 268 | const handlePromise = () => pFinally(Promise.all([
|
---|
| 269 | processDone,
|
---|
| 270 | getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}),
|
---|
| 271 | getStream(spawned, 'stderr', {encoding, buffer, maxBuffer})
|
---|
| 272 | ]).then(arr => {
|
---|
| 273 | const result = arr[0];
|
---|
| 274 | result.stdout = arr[1];
|
---|
| 275 | result.stderr = arr[2];
|
---|
| 276 |
|
---|
| 277 | if (result.error || result.code !== 0 || result.signal !== null) {
|
---|
| 278 | const err = makeError(result, {
|
---|
| 279 | joinedCmd,
|
---|
| 280 | parsed,
|
---|
| 281 | timedOut
|
---|
| 282 | });
|
---|
| 283 |
|
---|
| 284 | // TODO: missing some timeout logic for killed
|
---|
| 285 | // https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
|
---|
| 286 | // err.killed = spawned.killed || killed;
|
---|
| 287 | err.killed = err.killed || spawned.killed;
|
---|
| 288 |
|
---|
| 289 | if (!parsed.opts.reject) {
|
---|
| 290 | return err;
|
---|
| 291 | }
|
---|
| 292 |
|
---|
| 293 | throw err;
|
---|
| 294 | }
|
---|
| 295 |
|
---|
| 296 | return {
|
---|
| 297 | stdout: handleOutput(parsed.opts, result.stdout),
|
---|
| 298 | stderr: handleOutput(parsed.opts, result.stderr),
|
---|
| 299 | code: 0,
|
---|
| 300 | failed: false,
|
---|
| 301 | killed: false,
|
---|
| 302 | signal: null,
|
---|
| 303 | cmd: joinedCmd,
|
---|
| 304 | timedOut: false
|
---|
| 305 | };
|
---|
| 306 | }), destroy);
|
---|
| 307 |
|
---|
| 308 | crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed);
|
---|
| 309 |
|
---|
| 310 | handleInput(spawned, parsed.opts.input);
|
---|
| 311 |
|
---|
| 312 | spawned.then = (onfulfilled, onrejected) => handlePromise().then(onfulfilled, onrejected);
|
---|
| 313 | spawned.catch = onrejected => handlePromise().catch(onrejected);
|
---|
| 314 |
|
---|
| 315 | return spawned;
|
---|
| 316 | };
|
---|
| 317 |
|
---|
| 318 | // TODO: set `stderr: 'ignore'` when that option is implemented
|
---|
| 319 | module.exports.stdout = (...args) => module.exports(...args).then(x => x.stdout);
|
---|
| 320 |
|
---|
| 321 | // TODO: set `stdout: 'ignore'` when that option is implemented
|
---|
| 322 | module.exports.stderr = (...args) => module.exports(...args).then(x => x.stderr);
|
---|
| 323 |
|
---|
| 324 | module.exports.shell = (cmd, opts) => handleShell(module.exports, cmd, opts);
|
---|
| 325 |
|
---|
| 326 | module.exports.sync = (cmd, args, opts) => {
|
---|
| 327 | const parsed = handleArgs(cmd, args, opts);
|
---|
| 328 | const joinedCmd = joinCmd(cmd, args);
|
---|
| 329 |
|
---|
| 330 | if (isStream(parsed.opts.input)) {
|
---|
| 331 | throw new TypeError('The `input` option cannot be a stream in sync mode');
|
---|
| 332 | }
|
---|
| 333 |
|
---|
| 334 | const result = childProcess.spawnSync(parsed.cmd, parsed.args, parsed.opts);
|
---|
| 335 | result.code = result.status;
|
---|
| 336 |
|
---|
| 337 | if (result.error || result.status !== 0 || result.signal !== null) {
|
---|
| 338 | const err = makeError(result, {
|
---|
| 339 | joinedCmd,
|
---|
| 340 | parsed
|
---|
| 341 | });
|
---|
| 342 |
|
---|
| 343 | if (!parsed.opts.reject) {
|
---|
| 344 | return err;
|
---|
| 345 | }
|
---|
| 346 |
|
---|
| 347 | throw err;
|
---|
| 348 | }
|
---|
| 349 |
|
---|
| 350 | return {
|
---|
| 351 | stdout: handleOutput(parsed.opts, result.stdout),
|
---|
| 352 | stderr: handleOutput(parsed.opts, result.stderr),
|
---|
| 353 | code: 0,
|
---|
| 354 | failed: false,
|
---|
| 355 | signal: null,
|
---|
| 356 | cmd: joinedCmd,
|
---|
| 357 | timedOut: false
|
---|
| 358 | };
|
---|
| 359 | };
|
---|
| 360 |
|
---|
| 361 | module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);
|
---|