index.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. 'use strict';
  2. const debug = require('debug')('runscript');
  3. const is = require('is-type-of');
  4. const assert = require('assert');
  5. const path = require('path');
  6. const spawn = require('child_process').spawn;
  7. /**
  8. * Run shell script in child process
  9. * Support OSX, Linux and Windows
  10. * @param {String} script - full script string, like `git clone https://github.com/node-modules/runscript.git`
  11. * @param {Object} [options] - spawn options
  12. * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
  13. * @param {Object} [extraOptions] - extra options for running
  14. * - {Number} [extraOptions.timeout] - child process running timeout
  15. * @return {Object} stdio object, will contains stdio.stdout and stdio.stderr buffer.
  16. */
  17. module.exports = function runScript(script, options, extraOptions) {
  18. return new Promise((resolve, reject) => {
  19. extraOptions = extraOptions || {};
  20. options = options || {};
  21. options.env = options.env || Object.assign({}, process.env);
  22. options.cwd = options.cwd || process.cwd();
  23. options.stdio = options.stdio || 'inherit';
  24. if (options.stdout) assert(is.writableStream(options.stdout), 'options.stdout should be writable stream');
  25. if (options.stderr) assert(is.writableStream(options.stderr), 'options.stderr should be writable stream');
  26. let sh = 'sh';
  27. let shFlag = '-c';
  28. if (process.platform === 'win32') {
  29. sh = process.env.comspec || 'cmd';
  30. shFlag = '/d /s /c';
  31. options.windowsVerbatimArguments = true;
  32. if (script.indexOf('./') === 0 || script.indexOf('.\\') === 0 ||
  33. script.indexOf('../') === 0 || script.indexOf('..\\') === 0) {
  34. const splits = script.split(' ');
  35. splits[0] = path.join(options.cwd, splits[0]);
  36. script = splits.join(' ');
  37. }
  38. }
  39. debug('%s %s %s, %j, %j', sh, shFlag, script, options, extraOptions);
  40. const proc = spawn(sh, [ shFlag, script ], options);
  41. const stdout = [];
  42. const stderr = [];
  43. let isEnd = false;
  44. let timeoutTimer;
  45. if (proc.stdout) {
  46. proc.stdout.on('data', buf => {
  47. debug('stdout %d bytes', buf.length);
  48. stdout.push(buf);
  49. });
  50. if (options.stdout) {
  51. proc.stdout.pipe(options.stdout);
  52. }
  53. }
  54. if (proc.stderr) {
  55. proc.stderr.on('data', buf => {
  56. debug('stderr %d bytes', buf.length);
  57. stderr.push(buf);
  58. });
  59. if (options.stderr) {
  60. proc.stderr.pipe(options.stderr);
  61. }
  62. }
  63. proc.on('error', err => {
  64. debug('proc emit error: %s', err);
  65. if (isEnd) return;
  66. isEnd = true;
  67. clearTimeout(timeoutTimer);
  68. reject(err);
  69. });
  70. proc.on('exit', code => {
  71. debug('proc emit exit: %s', code);
  72. if (isEnd) return;
  73. isEnd = true;
  74. clearTimeout(timeoutTimer);
  75. const stdio = {
  76. stdout: null,
  77. stderr: null,
  78. };
  79. if (stdout.length > 0) {
  80. stdio.stdout = Buffer.concat(stdout);
  81. }
  82. if (stderr.length > 0) {
  83. stdio.stderr = Buffer.concat(stderr);
  84. }
  85. if (code !== 0) {
  86. const err = new Error(`Run "${sh} ${shFlag} ${script}" error, exit code ${code}`);
  87. err.name = 'RunScriptError';
  88. err.stdio = stdio;
  89. err.exitcode = code;
  90. return reject(err);
  91. }
  92. return resolve(stdio);
  93. });
  94. proc.on('close', code => {
  95. debug('proc emit close: %s', code);
  96. });
  97. if (typeof extraOptions.timeout === 'number' && extraOptions.timeout > 0) {
  98. // start timer
  99. timeoutTimer = setTimeout(() => {
  100. debug('proc run timeout: %dms', extraOptions.timeout);
  101. isEnd = true;
  102. debug('kill child process %s', proc.pid);
  103. proc.kill();
  104. const err = new Error(`Run "${sh} ${shFlag} ${script}" timeout in ${extraOptions.timeout}ms`);
  105. err.name = 'RunScriptTimeoutError';
  106. const stdio = {
  107. stdout: null,
  108. stderr: null,
  109. };
  110. if (stdout.length > 0) {
  111. stdio.stdout = Buffer.concat(stdout);
  112. }
  113. if (stderr.length > 0) {
  114. stdio.stderr = Buffer.concat(stderr);
  115. }
  116. err.stdio = stdio;
  117. return reject(err);
  118. }, extraOptions.timeout);
  119. }
  120. });
  121. };