'use strict'; const debug = require('debug')('common-bin'); const cp = require('child_process'); const is = require('is-type-of'); const unparse = require('dargs'); // 存储子进程 const childs = new Set(); //hooke标识,用来标识是否已经存在子进行了 let hadHook = false; //启动任意一下子进和的时候,就放置一个钩子, //并且只有第一次存放子进程的时候会放置钩子, //钩子只会执行一次(once的特性) function gracefull(proc) { // 保存子进程引用 childs.add(proc); // 设置一次钩子 /* istanbul ignore else */ if (!hadHook) { hadHook = true; let signal; [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => { process.once(event, () => { signal = event; process.exit(0); }); }); //主进程退出,自动销毁子进程 process.once('exit', () => { // had test at my-helper.test.js, but coffee can't collect coverage info. for (const child of childs) { debug('kill child %s with %s', child.pid, signal); child.kill(signal); } }); } } /** * 封装fork起动子进程, 返回promise并且优雅的退出 * @method helper#forkNode * @param {String} modulePath - bin path * @param {Array} [args] - arguments * @param {Object} [options] - options * @return {Promise} err or undefined * @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options */ exports.forkNode = (modulePath, args = [], options = {}) => { options.stdio = options.stdio || 'inherit'; debug('Run fork `%s %s %s`', process.execPath, modulePath, args.join(' ')); const proc = cp.fork(modulePath, args, options); gracefull(proc); return new Promise((resolve, reject) => { proc.once('exit', code => { childs.delete(proc); if (code !== 0) { const err = new Error(modulePath + ' ' + args + ' exit with code ' + code); err.code = code; reject(err); } else { resolve(); } }); }); }; /** * 封装fork起动子进程, 返回promise并且优雅的退出 * @method helper#forkNode * @param {String} cmd - command * @param {Array} [args] - arguments * @param {Object} [options] - options * @return {Promise} err or undefined * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options */ exports.spawn = (cmd, args = [], options = {}) => { options.stdio = options.stdio || 'inherit'; debug('Run spawn `%s %s`', cmd, args.join(' ')); return new Promise((resolve, reject) => { const proc = cp.spawn(cmd, args, options); gracefull(proc); proc.once('error', err => { /* istanbul ignore next */ reject(err); }); proc.once('exit', code => { childs.delete(proc); if (code !== 0) { return reject(new Error(`spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`)); } resolve(); }); }); }; /** * 执行npm install * @method helper#npmInstall * @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall` * @param {String} name - node module name * @param {String} cwd - target directory * @return {Promise} err or undefined */ exports.npmInstall = (npmCli, name, cwd) => { const options = { stdio: 'inherit', env: process.env, cwd, }; const args = [ 'i', name ]; console.log('[common-bin] `%s %s` to %s ...', npmCli, args.join(' '), options.cwd); return exports.spawn(npmCli, args, options); }; /** * 调用方法 * @method helper#callFn * @param {Function} fn - support generator / async / normal function return promise * @param {Array} [args] - fn args * @param {Object} [thisArg] - this * @return {Object} result */ exports.callFn = function* (fn, args = [], thisArg) { if (!is.function(fn)) return; if (is.generatorFunction(fn)) { return yield fn.apply(thisArg, args); } const r = fn.apply(thisArg, args); if (is.promise(r)) { return yield r; } return r; }; /** * unparse argv and change it to array style * @method helper#unparseArgv * @param {Object} argv - yargs style * @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs * @param {Array} [options.includes] - keys or regex of keys to include * @param {Array} [options.excludes] - keys or regex of keys to exclude * @return {Array} [ '--debug=7000', '--debug-brk' ] */ exports.unparseArgv = (argv, options = {}) => { // revert argv object to array // yargs will paser `debug-brk` to `debug-brk` and `debugBrk`, so we need to filter return [ ...new Set(unparse(argv, options)) ]; }; /** * extract execArgv from argv * @method helper#extractExecArgv * @param {Object} argv - yargs style * @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} } */ exports.extractExecArgv = argv => { const debugOptions = {}; const execArgvObj = {}; let debugPort; for (const key of Object.keys(argv)) { const value = argv[key]; // skip undefined set uppon (camel etc.) if (value === undefined) continue; // debug / debug-brk / debug-port / inspect / inspect-brk / inspect-port if ([ 'debug', 'debug-brk', 'debug-port', 'inspect', 'inspect-brk', 'inspect-port' ].includes(key)) { if (typeof value === 'number') debugPort = value; debugOptions[key] = argv[key]; execArgvObj[key] = argv[key]; } else if (match(key, [ 'es_staging', 'expose_debug_as', /^harmony.*/ ])) { execArgvObj[key] = argv[key]; } else if (key.startsWith('node-options--')) { // support node options, like: commond --node-options--trace-warnings => execArgv.push('--trace-warnings') execArgvObj[key.replace('node-options--', '')] = argv[key]; } } return { debugPort, debugOptions, execArgvObj }; }; function match(key, arr) { return arr.some(x => x instanceof RegExp ? x.test(key) : x === key); // eslint-disable-line no-confusing-arrow }