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