UNPKG

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