• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23
24const {
25  ArrayIsArray,
26  Error,
27  NumberIsInteger,
28  ObjectAssign,
29  ObjectDefineProperty,
30  ObjectPrototypeHasOwnProperty,
31  Promise,
32} = primordials;
33
34const {
35  promisify,
36  convertToValidSignal,
37  getSystemErrorName
38} = require('internal/util');
39const { isArrayBufferView } = require('internal/util/types');
40let debug = require('internal/util/debuglog').debuglog(
41  'child_process',
42  (fn) => {
43    debug = fn;
44  }
45);
46const { Buffer } = require('buffer');
47const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap');
48const {
49  ERR_INVALID_ARG_VALUE,
50  ERR_CHILD_PROCESS_IPC_REQUIRED,
51  ERR_CHILD_PROCESS_STDIO_MAXBUFFER,
52  ERR_INVALID_ARG_TYPE,
53  ERR_OUT_OF_RANGE
54} = require('internal/errors').codes;
55const { clearTimeout, setTimeout } = require('timers');
56const { validateString, isInt32 } = require('internal/validators');
57const child_process = require('internal/child_process');
58const {
59  getValidStdio,
60  setupChannel,
61  ChildProcess,
62  stdioStringToArray
63} = child_process;
64
65const MAX_BUFFER = 1024 * 1024;
66
67function fork(modulePath /* , args, options */) {
68  validateString(modulePath, 'modulePath');
69
70  // Get options and args arguments.
71  let execArgv;
72  let options = {};
73  let args = [];
74  let pos = 1;
75  if (pos < arguments.length && ArrayIsArray(arguments[pos])) {
76    args = arguments[pos++];
77  }
78
79  if (pos < arguments.length &&
80      (arguments[pos] === undefined || arguments[pos] === null)) {
81    pos++;
82  }
83
84  if (pos < arguments.length && arguments[pos] != null) {
85    if (typeof arguments[pos] !== 'object') {
86      throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]);
87    }
88
89    options = { ...arguments[pos++] };
90  }
91
92  // Prepare arguments for fork:
93  execArgv = options.execArgv || process.execArgv;
94
95  if (execArgv === process.execArgv && process._eval != null) {
96    const index = execArgv.lastIndexOf(process._eval);
97    if (index > 0) {
98      // Remove the -e switch to avoid fork bombing ourselves.
99      execArgv = execArgv.slice();
100      execArgv.splice(index - 1, 2);
101    }
102  }
103
104  args = execArgv.concat([modulePath], args);
105
106  if (typeof options.stdio === 'string') {
107    options.stdio = stdioStringToArray(options.stdio, 'ipc');
108  } else if (!ArrayIsArray(options.stdio)) {
109    // Use a separate fd=3 for the IPC channel. Inherit stdin, stdout,
110    // and stderr from the parent if silent isn't set.
111    options.stdio = stdioStringToArray(
112      options.silent ? 'pipe' : 'inherit',
113      'ipc');
114  } else if (!options.stdio.includes('ipc')) {
115    throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio');
116  }
117
118  options.execPath = options.execPath || process.execPath;
119  options.shell = false;
120
121  return spawn(options.execPath, args, options);
122}
123
124function _forkChild(fd, serializationMode) {
125  // set process.send()
126  const p = new Pipe(PipeConstants.IPC);
127  p.open(fd);
128  p.unref();
129  const control = setupChannel(process, p, serializationMode);
130  process.on('newListener', function onNewListener(name) {
131    if (name === 'message' || name === 'disconnect') control.ref();
132  });
133  process.on('removeListener', function onRemoveListener(name) {
134    if (name === 'message' || name === 'disconnect') control.unref();
135  });
136}
137
138function normalizeExecArgs(command, options, callback) {
139  if (typeof options === 'function') {
140    callback = options;
141    options = undefined;
142  }
143
144  // Make a shallow copy so we don't clobber the user's options object.
145  options = { ...options };
146  options.shell = typeof options.shell === 'string' ? options.shell : true;
147
148  return {
149    file: command,
150    options: options,
151    callback: callback
152  };
153}
154
155
156function exec(command, options, callback) {
157  const opts = normalizeExecArgs(command, options, callback);
158  return module.exports.execFile(opts.file,
159                                 opts.options,
160                                 opts.callback);
161}
162
163const customPromiseExecFunction = (orig) => {
164  return (...args) => {
165    let resolve;
166    let reject;
167    const promise = new Promise((res, rej) => {
168      resolve = res;
169      reject = rej;
170    });
171
172    promise.child = orig(...args, (err, stdout, stderr) => {
173      if (err !== null) {
174        err.stdout = stdout;
175        err.stderr = stderr;
176        reject(err);
177      } else {
178        resolve({ stdout, stderr });
179      }
180    });
181
182    return promise;
183  };
184};
185
186ObjectDefineProperty(exec, promisify.custom, {
187  enumerable: false,
188  value: customPromiseExecFunction(exec)
189});
190
191function execFile(file /* , args, options, callback */) {
192  let args = [];
193  let callback;
194  let options;
195
196  // Parse the optional positional parameters.
197  let pos = 1;
198  if (pos < arguments.length && ArrayIsArray(arguments[pos])) {
199    args = arguments[pos++];
200  } else if (pos < arguments.length && arguments[pos] == null) {
201    pos++;
202  }
203
204  if (pos < arguments.length && typeof arguments[pos] === 'object') {
205    options = arguments[pos++];
206  } else if (pos < arguments.length && arguments[pos] == null) {
207    pos++;
208  }
209
210  if (pos < arguments.length && typeof arguments[pos] === 'function') {
211    callback = arguments[pos++];
212  }
213
214  if (!callback && pos < arguments.length && arguments[pos] != null) {
215    throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
216  }
217
218  options = {
219    encoding: 'utf8',
220    timeout: 0,
221    maxBuffer: MAX_BUFFER,
222    killSignal: 'SIGTERM',
223    cwd: null,
224    env: null,
225    shell: false,
226    ...options
227  };
228
229  // Validate the timeout, if present.
230  validateTimeout(options.timeout);
231
232  // Validate maxBuffer, if present.
233  validateMaxBuffer(options.maxBuffer);
234
235  options.killSignal = sanitizeKillSignal(options.killSignal);
236
237  const child = spawn(file, args, {
238    cwd: options.cwd,
239    env: options.env,
240    gid: options.gid,
241    uid: options.uid,
242    shell: options.shell,
243    windowsHide: !!options.windowsHide,
244    windowsVerbatimArguments: !!options.windowsVerbatimArguments
245  });
246
247  let encoding;
248  const _stdout = [];
249  const _stderr = [];
250  if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
251    encoding = options.encoding;
252  } else {
253    encoding = null;
254  }
255  let stdoutLen = 0;
256  let stderrLen = 0;
257  let killed = false;
258  let exited = false;
259  let timeoutId;
260
261  let ex = null;
262
263  let cmd = file;
264
265  function exithandler(code, signal) {
266    if (exited) return;
267    exited = true;
268
269    if (timeoutId) {
270      clearTimeout(timeoutId);
271      timeoutId = null;
272    }
273
274    if (!callback) return;
275
276    // merge chunks
277    let stdout;
278    let stderr;
279    if (encoding ||
280      (
281        child.stdout &&
282        child.stdout.readableEncoding
283      )) {
284      stdout = _stdout.join('');
285    } else {
286      stdout = Buffer.concat(_stdout);
287    }
288    if (encoding ||
289      (
290        child.stderr &&
291        child.stderr.readableEncoding
292      )) {
293      stderr = _stderr.join('');
294    } else {
295      stderr = Buffer.concat(_stderr);
296    }
297
298    if (!ex && code === 0 && signal === null) {
299      callback(null, stdout, stderr);
300      return;
301    }
302
303    if (args.length !== 0)
304      cmd += ` ${args.join(' ')}`;
305
306    if (!ex) {
307      // eslint-disable-next-line no-restricted-syntax
308      ex = new Error('Command failed: ' + cmd + '\n' + stderr);
309      ex.killed = child.killed || killed;
310      ex.code = code < 0 ? getSystemErrorName(code) : code;
311      ex.signal = signal;
312    }
313
314    ex.cmd = cmd;
315    callback(ex, stdout, stderr);
316  }
317
318  function errorhandler(e) {
319    ex = e;
320
321    if (child.stdout)
322      child.stdout.destroy();
323
324    if (child.stderr)
325      child.stderr.destroy();
326
327    exithandler();
328  }
329
330  function kill() {
331    if (child.stdout)
332      child.stdout.destroy();
333
334    if (child.stderr)
335      child.stderr.destroy();
336
337    killed = true;
338    try {
339      child.kill(options.killSignal);
340    } catch (e) {
341      ex = e;
342      exithandler();
343    }
344  }
345
346  if (options.timeout > 0) {
347    timeoutId = setTimeout(function delayedKill() {
348      kill();
349      timeoutId = null;
350    }, options.timeout);
351  }
352
353  if (child.stdout) {
354    if (encoding)
355      child.stdout.setEncoding(encoding);
356
357    child.stdout.on('data', function onChildStdout(chunk) {
358      const encoding = child.stdout.readableEncoding;
359      const length = encoding ?
360        Buffer.byteLength(chunk, encoding) :
361        chunk.length;
362      stdoutLen += length;
363
364      if (stdoutLen > options.maxBuffer) {
365        const truncatedLen = options.maxBuffer - (stdoutLen - length);
366        _stdout.push(chunk.slice(0, truncatedLen));
367
368        ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout');
369        kill();
370      } else {
371        _stdout.push(chunk);
372      }
373    });
374  }
375
376  if (child.stderr) {
377    if (encoding)
378      child.stderr.setEncoding(encoding);
379
380    child.stderr.on('data', function onChildStderr(chunk) {
381      const encoding = child.stderr.readableEncoding;
382      const length = encoding ?
383        Buffer.byteLength(chunk, encoding) :
384        chunk.length;
385      stderrLen += length;
386
387      if (stderrLen > options.maxBuffer) {
388        const truncatedLen = options.maxBuffer - (stderrLen - length);
389        _stderr.push(chunk.slice(0, truncatedLen));
390
391        ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stderr');
392        kill();
393      } else {
394        _stderr.push(chunk);
395      }
396    });
397  }
398
399  child.addListener('close', exithandler);
400  child.addListener('error', errorhandler);
401
402  return child;
403}
404
405ObjectDefineProperty(execFile, promisify.custom, {
406  enumerable: false,
407  value: customPromiseExecFunction(execFile)
408});
409
410function normalizeSpawnArguments(file, args, options) {
411  validateString(file, 'file');
412
413  if (file.length === 0)
414    throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty');
415
416  if (ArrayIsArray(args)) {
417    args = args.slice(0);
418  } else if (args == null) {
419    args = [];
420  } else if (typeof args !== 'object') {
421    throw new ERR_INVALID_ARG_TYPE('args', 'object', args);
422  } else {
423    options = args;
424    args = [];
425  }
426
427  if (options === undefined)
428    options = {};
429  else if (options === null || typeof options !== 'object')
430    throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
431
432  // Validate the cwd, if present.
433  if (options.cwd != null &&
434      typeof options.cwd !== 'string') {
435    throw new ERR_INVALID_ARG_TYPE('options.cwd', 'string', options.cwd);
436  }
437
438  // Validate detached, if present.
439  if (options.detached != null &&
440      typeof options.detached !== 'boolean') {
441    throw new ERR_INVALID_ARG_TYPE('options.detached',
442                                   'boolean', options.detached);
443  }
444
445  // Validate the uid, if present.
446  if (options.uid != null && !isInt32(options.uid)) {
447    throw new ERR_INVALID_ARG_TYPE('options.uid', 'int32', options.uid);
448  }
449
450  // Validate the gid, if present.
451  if (options.gid != null && !isInt32(options.gid)) {
452    throw new ERR_INVALID_ARG_TYPE('options.gid', 'int32', options.gid);
453  }
454
455  // Validate the shell, if present.
456  if (options.shell != null &&
457      typeof options.shell !== 'boolean' &&
458      typeof options.shell !== 'string') {
459    throw new ERR_INVALID_ARG_TYPE('options.shell',
460                                   ['boolean', 'string'], options.shell);
461  }
462
463  // Validate argv0, if present.
464  if (options.argv0 != null &&
465      typeof options.argv0 !== 'string') {
466    throw new ERR_INVALID_ARG_TYPE('options.argv0', 'string', options.argv0);
467  }
468
469  // Validate windowsHide, if present.
470  if (options.windowsHide != null &&
471      typeof options.windowsHide !== 'boolean') {
472    throw new ERR_INVALID_ARG_TYPE('options.windowsHide',
473                                   'boolean', options.windowsHide);
474  }
475
476  // Validate windowsVerbatimArguments, if present.
477  if (options.windowsVerbatimArguments != null &&
478      typeof options.windowsVerbatimArguments !== 'boolean') {
479    throw new ERR_INVALID_ARG_TYPE('options.windowsVerbatimArguments',
480                                   'boolean',
481                                   options.windowsVerbatimArguments);
482  }
483
484  // Make a shallow copy so we don't clobber the user's options object.
485  options = { ...options };
486
487  if (options.shell) {
488    const command = [file].concat(args).join(' ');
489    // Set the shell, switches, and commands.
490    if (process.platform === 'win32') {
491      if (typeof options.shell === 'string')
492        file = options.shell;
493      else
494        file = process.env.comspec || 'cmd.exe';
495      // '/d /s /c' is used only for cmd.exe.
496      if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) {
497        args = ['/d', '/s', '/c', `"${command}"`];
498        options.windowsVerbatimArguments = true;
499      } else {
500        args = ['-c', command];
501      }
502    } else {
503      if (typeof options.shell === 'string')
504        file = options.shell;
505      else if (process.platform === 'android')
506        file = '/system/bin/sh';
507      else
508        file = '/bin/sh';
509      args = ['-c', command];
510    }
511  }
512
513  if (typeof options.argv0 === 'string') {
514    args.unshift(options.argv0);
515  } else {
516    args.unshift(file);
517  }
518
519  const env = options.env || process.env;
520  const envPairs = [];
521
522  // process.env.NODE_V8_COVERAGE always propagates, making it possible to
523  // collect coverage for programs that spawn with white-listed environment.
524  if (process.env.NODE_V8_COVERAGE &&
525      !ObjectPrototypeHasOwnProperty(options.env || {}, 'NODE_V8_COVERAGE')) {
526    env.NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE;
527  }
528
529  // Prototype values are intentionally included.
530  for (const key in env) {
531    const value = env[key];
532    if (value !== undefined) {
533      envPairs.push(`${key}=${value}`);
534    }
535  }
536
537  return {
538    file: file,
539    args: args,
540    options: options,
541    envPairs: envPairs
542  };
543}
544
545
546function spawn(file, args, options) {
547  const opts = normalizeSpawnArguments(file, args, options);
548  const child = new ChildProcess();
549
550  options = opts.options;
551  debug('spawn', opts.args, options);
552
553  child.spawn({
554    file: opts.file,
555    args: opts.args,
556    cwd: options.cwd,
557    windowsHide: !!options.windowsHide,
558    windowsVerbatimArguments: !!options.windowsVerbatimArguments,
559    detached: !!options.detached,
560    envPairs: opts.envPairs,
561    stdio: options.stdio,
562    uid: options.uid,
563    gid: options.gid,
564    serialization: options.serialization,
565  });
566
567  return child;
568}
569
570function spawnSync(file, args, options) {
571  const opts = normalizeSpawnArguments(file, args, options);
572
573  const defaults = {
574    maxBuffer: MAX_BUFFER,
575    ...opts.options
576  };
577  options = opts.options = defaults;
578
579  debug('spawnSync', opts.args, options);
580
581  // Validate the timeout, if present.
582  validateTimeout(options.timeout);
583
584  // Validate maxBuffer, if present.
585  validateMaxBuffer(options.maxBuffer);
586
587  options.file = opts.file;
588  options.args = opts.args;
589  options.envPairs = opts.envPairs;
590
591  // Validate and translate the kill signal, if present.
592  options.killSignal = sanitizeKillSignal(options.killSignal);
593
594  options.stdio = getValidStdio(options.stdio || 'pipe', true).stdio;
595
596  if (options.input) {
597    const stdin = options.stdio[0] = { ...options.stdio[0] };
598    stdin.input = options.input;
599  }
600
601  // We may want to pass data in on any given fd, ensure it is a valid buffer
602  for (let i = 0; i < options.stdio.length; i++) {
603    const input = options.stdio[i] && options.stdio[i].input;
604    if (input != null) {
605      const pipe = options.stdio[i] = { ...options.stdio[i] };
606      if (isArrayBufferView(input)) {
607        pipe.input = input;
608      } else if (typeof input === 'string') {
609        pipe.input = Buffer.from(input, options.encoding);
610      } else {
611        throw new ERR_INVALID_ARG_TYPE(`options.stdio[${i}]`,
612                                       ['Buffer',
613                                        'TypedArray',
614                                        'DataView',
615                                        'string'],
616                                       input);
617      }
618    }
619  }
620
621  return child_process.spawnSync(opts);
622}
623
624
625function checkExecSyncError(ret, args, cmd) {
626  let err;
627  if (ret.error) {
628    err = ret.error;
629  } else if (ret.status !== 0) {
630    let msg = 'Command failed: ';
631    msg += cmd || args.join(' ');
632    if (ret.stderr && ret.stderr.length > 0)
633      msg += `\n${ret.stderr.toString()}`;
634    // eslint-disable-next-line no-restricted-syntax
635    err = new Error(msg);
636  }
637  if (err) {
638    ObjectAssign(err, ret);
639  }
640  return err;
641}
642
643
644function execFileSync(command, args, options) {
645  const opts = normalizeSpawnArguments(command, args, options);
646  const inheritStderr = !opts.options.stdio;
647
648  const ret = spawnSync(opts.file, opts.args.slice(1), opts.options);
649
650  if (inheritStderr && ret.stderr)
651    process.stderr.write(ret.stderr);
652
653  const err = checkExecSyncError(ret, opts.args, undefined);
654
655  if (err)
656    throw err;
657
658  return ret.stdout;
659}
660
661
662function execSync(command, options) {
663  const opts = normalizeExecArgs(command, options, null);
664  const inheritStderr = !opts.options.stdio;
665
666  const ret = spawnSync(opts.file, opts.options);
667
668  if (inheritStderr && ret.stderr)
669    process.stderr.write(ret.stderr);
670
671  const err = checkExecSyncError(ret, opts.args, command);
672
673  if (err)
674    throw err;
675
676  return ret.stdout;
677}
678
679
680function validateTimeout(timeout) {
681  if (timeout != null && !(NumberIsInteger(timeout) && timeout >= 0)) {
682    throw new ERR_OUT_OF_RANGE('timeout', 'an unsigned integer', timeout);
683  }
684}
685
686
687function validateMaxBuffer(maxBuffer) {
688  if (maxBuffer != null && !(typeof maxBuffer === 'number' && maxBuffer >= 0)) {
689    throw new ERR_OUT_OF_RANGE('options.maxBuffer',
690                               'a positive number',
691                               maxBuffer);
692  }
693}
694
695
696function sanitizeKillSignal(killSignal) {
697  if (typeof killSignal === 'string' || typeof killSignal === 'number') {
698    return convertToValidSignal(killSignal);
699  } else if (killSignal != null) {
700    throw new ERR_INVALID_ARG_TYPE('options.killSignal',
701                                   ['string', 'number'],
702                                   killSignal);
703  }
704}
705
706module.exports = {
707  _forkChild,
708  ChildProcess,
709  exec,
710  execFile,
711  execFileSync,
712  execSync,
713  fork,
714  spawn,
715  spawnSync
716};
717