helper.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. 'use strict';
  2. const debug = require('debug')('common-bin');
  3. const cp = require('child_process');
  4. const is = require('is-type-of');
  5. const unparse = require('dargs');
  6. // only hook once and only when ever start any child.
  7. const childs = new Set();
  8. let hadHook = false;
  9. function gracefull(proc) {
  10. // save child ref
  11. childs.add(proc);
  12. // only hook once
  13. /* istanbul ignore else */
  14. if (!hadHook) {
  15. hadHook = true;
  16. let signal;
  17. [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
  18. process.once(event, () => {
  19. signal = event;
  20. process.exit(0);
  21. });
  22. });
  23. process.once('exit', () => {
  24. // had test at my-helper.test.js, but coffee can't collect coverage info.
  25. for (const child of childs) {
  26. debug('kill child %s with %s', child.pid, signal);
  27. child.kill(signal);
  28. }
  29. });
  30. }
  31. }
  32. /**
  33. * fork child process, wrap with promise and gracefull exit
  34. * @function helper#forkNode
  35. * @param {String} modulePath - bin path
  36. * @param {Array} [args] - arguments
  37. * @param {Object} [options] - options
  38. * @return {Promise} err or undefined
  39. * @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
  40. */
  41. exports.forkNode = (modulePath, args = [], options = {}) => {
  42. options.stdio = options.stdio || 'inherit';
  43. debug('Run fork `%s %s %s`', process.execPath, modulePath, args.join(' '));
  44. const proc = cp.fork(modulePath, args, options);
  45. gracefull(proc);
  46. const promise = new Promise((resolve, reject) => {
  47. proc.once('exit', code => {
  48. childs.delete(proc);
  49. if (code !== 0) {
  50. const err = new Error(modulePath + ' ' + args + ' exit with code ' + code);
  51. err.code = code;
  52. reject(err);
  53. } else {
  54. resolve();
  55. }
  56. });
  57. });
  58. promise.proc = proc;
  59. return promise;
  60. };
  61. /**
  62. * spawn a new process, wrap with promise and gracefull exit
  63. * @function helper#forkNode
  64. * @param {String} cmd - command
  65. * @param {Array} [args] - arguments
  66. * @param {Object} [options] - options
  67. * @return {Promise} err or undefined
  68. * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
  69. */
  70. exports.spawn = (cmd, args = [], options = {}) => {
  71. options.stdio = options.stdio || 'inherit';
  72. debug('Run spawn `%s %s`', cmd, args.join(' '));
  73. return new Promise((resolve, reject) => {
  74. const proc = cp.spawn(cmd, args, options);
  75. gracefull(proc);
  76. proc.once('error', err => {
  77. /* istanbul ignore next */
  78. reject(err);
  79. });
  80. proc.once('exit', code => {
  81. childs.delete(proc);
  82. if (code !== 0) {
  83. return reject(new Error(`spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`));
  84. }
  85. resolve();
  86. });
  87. });
  88. };
  89. /**
  90. * exec npm install
  91. * @function helper#npmInstall
  92. * @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall`
  93. * @param {String} name - node module name
  94. * @param {String} cwd - target directory
  95. * @return {Promise} err or undefined
  96. */
  97. exports.npmInstall = (npmCli, name, cwd) => {
  98. const options = {
  99. stdio: 'inherit',
  100. env: process.env,
  101. cwd,
  102. };
  103. const args = [ 'i', name ];
  104. console.log('[common-bin] `%s %s` to %s ...', npmCli, args.join(' '), options.cwd);
  105. return exports.spawn(npmCli, args, options);
  106. };
  107. /**
  108. * call fn
  109. * @function helper#callFn
  110. * @param {Function} fn - support generator / async / normal function return promise
  111. * @param {Array} [args] - fn args
  112. * @param {Object} [thisArg] - this
  113. * @return {Object} result
  114. */
  115. exports.callFn = function* (fn, args = [], thisArg) {
  116. if (!is.function(fn)) return;
  117. if (is.generatorFunction(fn)) {
  118. return yield fn.apply(thisArg, args);
  119. }
  120. const r = fn.apply(thisArg, args);
  121. if (is.promise(r)) {
  122. return yield r;
  123. }
  124. return r;
  125. };
  126. /**
  127. * unparse argv and change it to array style
  128. * @function helper#unparseArgv
  129. * @param {Object} argv - yargs style
  130. * @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs
  131. * @param {Array} [options.includes] - keys or regex of keys to include
  132. * @param {Array} [options.excludes] - keys or regex of keys to exclude
  133. * @return {Array} [ '--debug=7000', '--debug-brk' ]
  134. */
  135. exports.unparseArgv = (argv, options = {}) => {
  136. // revert argv object to array
  137. // yargs will paser `debug-brk` to `debug-brk` and `debugBrk`, so we need to filter
  138. return [ ...new Set(unparse(argv, options)) ];
  139. };
  140. /**
  141. * extract execArgv from argv
  142. * @function helper#extractExecArgv
  143. * @param {Object} argv - yargs style
  144. * @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} }
  145. */
  146. exports.extractExecArgv = argv => {
  147. const debugOptions = {};
  148. const execArgvObj = {};
  149. let debugPort;
  150. for (const key of Object.keys(argv)) {
  151. const value = argv[key];
  152. // skip undefined set uppon (camel etc.)
  153. if (value === undefined) continue;
  154. // debug / debug-brk / debug-port / inspect / inspect-brk / inspect-port
  155. if ([ 'debug', 'debug-brk', 'debug-port', 'inspect', 'inspect-brk', 'inspect-port' ].includes(key)) {
  156. if (typeof value === 'number') debugPort = value;
  157. debugOptions[key] = argv[key];
  158. execArgvObj[key] = argv[key];
  159. } else if (match(key, [ 'es_staging', 'expose_debug_as', /^harmony.*/ ])) {
  160. execArgvObj[key] = argv[key];
  161. } else if (key.startsWith('node-options--')) {
  162. // support node options, like: commond --node-options--trace-warnings => execArgv.push('--trace-warnings')
  163. execArgvObj[key.replace('node-options--', '')] = argv[key];
  164. }
  165. }
  166. return { debugPort, debugOptions, execArgvObj };
  167. };
  168. function match(key, arr) {
  169. return arr.some(x => x instanceof RegExp ? x.test(key) : x === key); // eslint-disable-line no-confusing-arrow
  170. }