common-bin
Version:
Abstraction bin tool
190 lines (170 loc) • 5.78 kB
JavaScript
const debug = require('debug')('common-bin');
const cp = require('child_process');
const is = require('is-type-of');
const unparse = require('dargs');
// only hook once and only when ever start any child.
const childs = new Set();
let hadHook = false;
function gracefull(proc) {
// save child ref
childs.add(proc);
// only hook once
/* 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 child process, wrap with promise and gracefull exit
* @function 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);
const promise = 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();
}
});
});
promise.proc = proc;
return promise;
};
/**
* spawn a new process, wrap with promise and gracefull exit
* @function 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();
});
});
};
/**
* exec npm install
* @function 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 = async (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 await exports.spawn(npmCli, args, options);
};
/**
* call fn
* @function helper#callFn
* @param {Function} fn - support async / normal function return promise
* @param {Array} [args] - fn args
* @param {Object} [thisArg] - this
* @return {Object} result
*/
exports.callFn = async (fn, args = [], thisArg) => {
if (!is.function(fn)) return;
if (is.generatorFunction(fn)) {
throw new TypeError('helper.callFn don\'t support generator function');
}
const r = fn.apply(thisArg, args);
if (is.promise(r)) {
return await r;
}
return r;
};
/**
* unparse argv and change it to array style
* @function 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
* @function 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
}
;