File: /var/www/tana/frontend/node_modules/concurrently/src/main.js
#!/usr/bin/env node
'use strict';
const Rx = require('rx');
const path = require('path');
const formatDate = require('date-fns/format');
const program = require('commander');
const _ = require('lodash');
const treeKill = require('tree-kill');
const chalk = require('chalk');
const spawn = require('spawn-command');
const supportsColor = require('supports-color');
const IS_WINDOWS = /^win/.test(process.platform);
const findChild = require('./findChild.js');
const parseCmds = require('./parseCmds');
let config = {
// Kill other processes if one dies
killOthers: false,
// Kill other processes if one exits with non zero status code
killOthersOnFail: false,
// Return success or failure of the 'first' child to terminate, the 'last' child,
// or succeed only if 'all' children succeed
success: 'all',
// Prefix logging with pid
// Possible values: 'pid', 'none', 'time', 'command', 'index', 'name'
prefix: '',
// List of custom names to be used in prefix template
names: '',
// What to split the list of custom names on
nameSeparator: ',',
// Comma-separated list of chalk color paths to use on prefixes.
prefixColors: 'gray.dim',
// moment/date-fns format
timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS',
// How many characters to display from start of command in prefix if
// command is defined. Note that also '..' will be added in the middle
prefixLength: 10,
// By default, color output
color: true,
// If true, the output will only be raw output of processes, nothing more
raw: false,
// If true, the process restart when it exited with status code non-zero
allowRestart: false,
// By default, restart instantly
restartAfter: 0,
// By default, restart once
restartTries: 1,
// Default identifier for child to which input on stdin should be sent.
defaultInputTarget: '0'
};
function main() {
const firstBase = path.basename(process.argv[0]);
const secondBase = path.basename(process.argv[1]);
if (firstBase === 'concurrent' || secondBase === 'concurrent') {
console.error('Warning: "concurrent" command is deprecated, use "concurrently" instead.\n');
}
parseArgs();
config = mergeDefaultsWithArgs(config);
const cmds = parseCmds(program.args, config);
run(cmds);
}
function parseArgs() {
program
.version(require('../package.json').version)
.usage('[options] <command ...>')
.option(
'-k, --kill-others',
'kill other processes if one exits or dies'
)
.option(
'--kill-others-on-fail',
'kill other processes if one exits with non zero status code'
)
.option(
'--no-color',
'disable colors from logging'
)
.option(
'-p, --prefix <prefix>',
'prefix used in logging for each process.\n' +
'Possible values: index, pid, time, command, name, none, or a template. Default: ' +
'index or name (when --names is set). Example template: "{time}-{pid}"\n'
)
.option(
'-n, --names <names>',
'List of custom names to be used in prefix template.\n' +
'Example names: "main,browser,server"\n'
)
.option(
'--name-separator <char>',
'The character to split <names> on.\n' +
'Default: "' + config.nameSeparator + '". Example usage: ' +
'concurrently -n "styles,scripts|server" --name-separator "|" <command ...>\n'
)
.option(
'-c, --prefix-colors <colors>',
'Comma-separated list of chalk colors to use on prefixes. If there are more commands than colors, the last color will be repeated.\n' +
'Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
'Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray\n' +
'Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
'See https://www.npmjs.com/package/chalk for more information.\n' +
'Default: "' + config.prefixColors + '". Example: "black.bgWhite,cyan,gray.dim"\n'
)
.option(
'-t, --timestamp-format <format>',
'specify the timestamp in moment/date-fns format. Default: ' +
config.timestampFormat + '\n'
)
.option(
'-r, --raw',
'output only raw output of processes,' +
' disables prettifying and concurrently coloring'
)
.option(
'-s, --success <first|last|all>',
'Return exit code of zero or one based on the success or failure ' +
'of the "first" child to terminate, the "last" child, or succeed ' +
' only if "all" child processes succeed. Default: ' +
config.success + '\n'
)
.option(
'-l, --prefix-length <length>',
'limit how many characters of the command is displayed in prefix.\n' +
'The option can be used to shorten long commands.\n' +
'Works only if prefix is set to "command". Default: ' +
config.prefixLength + '\n'
)
.option(
'--allow-restart',
'Restart a process which died. Default: ' +
config.allowRestart + '\n'
)
.option(
'--restart-after <miliseconds>',
'delay time to respawn the process. Default: ' +
config.restartAfter + '\n'
)
.option(
'--restart-tries <times>',
'limit the number of respawn tries. Default: ' +
config.restartTries + '\n'
)
.option(
'--default-input-target <identifier>',
'identifier for child process to which input on ' +
'stdin should be sent if not specified at start ' +
'of input. Can be either the index or the name ' +
'of the process. Default: ' + config.defaultInputTarget + '\n'
);
program.on('--help', function() {
const help = [
' Input:',
'',
' Input can be sent to any of the child processes using either the name or',
' index of the command followed by a colon. If no child identifier is',
' specified then the input will be sent to the child specified by the',
' `--default-input-target` option, which defaults to index 0.',
'',
' Examples:',
'',
' - Kill other processes if one exits or dies',
'',
' $ concurrently --kill-others "grunt watch" "http-server"',
'',
' - Kill other processes if one exits with non zero status code',
'',
' $ concurrently --kill-others-on-fail "npm run build:client" "npm run build:server"',
'',
' - Output nothing more than stdout+stderr of child processes',
'',
' $ concurrently --raw "npm run watch-less" "npm run watch-js"',
'',
' - Normal output but without colors e.g. when logging to file',
'',
' $ concurrently --no-color "grunt watch" "http-server" > log',
'',
' - Custom prefix',
'',
' $ concurrently --prefix "{time}-{pid}" "npm run watch" "http-server"',
'',
' - Custom names and colored prefixes',
'',
' $ concurrently --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"',
'',
' - Send input to default',
'',
' $ concurrently "nodemon" "npm run watch-js"',
' rs # Sends rs command to nodemon process',
'',
' - Specify a default-input-target',
'',
' $ concurrently --default-input-target 1 "npm run watch-js" nodemon',
' rs',
'',
' - Send input to specific child identified by index',
'',
' $ concurrently "npm run watch-js" nodemon',
' 1:rs',
'',
' - Send input to specific child identified by name',
'',
' $ concurrently -n js,srv "npm run watch-js" nodemon',
' srv:rs',
'',
' - Shortened NPM run commands',
'',
' $ concurrently npm:watch-node npm:watch-js npm:watch-css',
'',
' - Shortened NPM run command with wildcard',
'',
' $ concurrently npm:watch-*',
''
];
console.log(help.join('\n'));
const url = 'https://github.com/kimmobrunfeldt/concurrently';
console.log(' For more details, visit ' + url);
console.log('');
});
program.parse(process.argv);
}
function mergeDefaultsWithArgs(config) {
// This will pollute config object with other attributes from program too
return _.merge(config, program);
}
function run(commands) {
const childrenInfo = {};
let lastPrefixColor = _.get(chalk, chalk.gray.dim);
const children = _.map(commands, function(cmdInfo, index) {
const spawnOpts = config.raw ? {stdio: 'inherit'} : {};
if (IS_WINDOWS) {
spawnOpts.detached = false;
}
if (supportsColor) {
spawnOpts.env = Object.assign({FORCE_COLOR: supportsColor.level}, process.env);
}
const child = spawnChild(cmdInfo.cmd, spawnOpts);
if (cmdInfo.color) {
const prefixColorPath = cmdInfo.color;
lastPrefixColor = _.get(chalk, prefixColorPath, chalk.gray.dim);
}
childrenInfo[child.pid] = {
command: cmdInfo.cmd,
index: index,
name: cmdInfo.name,
options: spawnOpts,
restartTries: config.restartTries,
prefixColor: lastPrefixColor
};
return child;
});
const streams = toStreams(children);
handleChildEvents(streams, children, childrenInfo);
['SIGINT', 'SIGTERM'].forEach(function(signal) {
process.on(signal, function() {
children.forEach(function(child) {
treeKill(child.pid, signal);
});
});
});
process.stdin.on('data', (chunk) => {
let line = chunk.toString();
let targetId = config.defaultInputTarget;
if (line.indexOf(':') >= 0) {
const parts = line.split(':');
targetId = parts[0];
line = parts[1];
}
const target = findChild(targetId, children, childrenInfo);
if (target) {
target.stdin.write(line);
} else {
logError('', chalk.gray.dim, `Unable to find command [${targetId}]\n`);
}
});
}
function spawnChild(cmd, options) {
let child;
try {
child = spawn(cmd, options);
} catch (e) {
logError('', chalk.gray.dim, 'Error occured when executing command: ' + cmd);
logError('', chalk.gray.dim, e.stack);
process.exit(1);
}
return child;
}
function toStreams(children) {
// Transform all process events to rx streams
return _.map(children, function(child) {
const childStreams = {
error: Rx.Node.fromEvent(child, 'error'),
close: Rx.Node.fromEvent(child, 'close')
};
if (!config.raw) {
childStreams.stdout = Rx.Node.fromReadableStream(child.stdout);
childStreams.stderr = Rx.Node.fromReadableStream(child.stderr);
}
return _.reduce(childStreams, function(memo, stream, key) {
memo[key] = stream.map(function(data) {
return {child: child, data: data};
});
return memo;
}, {});
});
}
function handleChildEvents(streams, children, childrenInfo) {
handleClose(streams, children, childrenInfo);
handleError(streams, childrenInfo);
if (!config.raw) {
handleOutput(streams, childrenInfo, 'stdout');
handleOutput(streams, childrenInfo, 'stderr');
}
}
function handleOutput(streams, childrenInfo, source) {
const sourceStreams = _.map(streams, source);
const combinedSourceStream = Rx.Observable.merge.apply(this, sourceStreams);
combinedSourceStream.subscribe(function(event) {
const prefix = getPrefix(childrenInfo, event.child);
const prefixColor = childrenInfo[event.child.pid].prefixColor;
log(prefix, prefixColor, event.data.toString());
});
}
function handleClose(streams, children, childrenInfo) {
let aliveChildren = _.clone(children);
const exitCodes = [];
const closeStreams = _.map(streams, 'close');
const closeStream = Rx.Observable.merge.apply(this, closeStreams);
let othersKilled = false;
// TODO: Is it possible that amount of close events !== count of spawned?
closeStream.subscribe(function(event) {
const exitCode = event.data;
const nonSuccess = exitCode !== 0;
exitCodes.push(exitCode);
const prefix = getPrefix(childrenInfo, event.child);
const childInfo = childrenInfo[event.child.pid];
const prefixColor = childInfo.prefixColor;
const command = childInfo.command;
logEvent(prefix, prefixColor, command + ' exited with code ' + exitCode);
aliveChildren = _.filter(aliveChildren, function(child) {
return child.pid !== event.child.pid;
});
if (nonSuccess && config.allowRestart && childInfo.restartTries--) {
respawnChild(event, childrenInfo);
return;
}
if (aliveChildren.length === 0) {
exit(exitCodes);
}
if (!othersKilled) {
if (config.killOthers) {
killOtherProcesses(aliveChildren);
othersKilled = true;
} else if (config.killOthersOnFail && nonSuccess) {
killOtherProcesses(aliveChildren);
othersKilled = true;
}
}
});
}
function respawnChild(event, childrenInfo) {
setTimeout(function() {
const childInfo = childrenInfo[event.child.pid];
const prefix = getPrefix(childrenInfo, event.child);
const prefixColor = childInfo.prefixColor;
logEvent(prefix, prefixColor, childInfo.command + ' restarted');
const newChild = spawnChild(childInfo.command, childInfo.options);
childrenInfo[newChild.pid] = childrenInfo[event.child.pid];
delete childrenInfo[event.child.pid];
const children = [newChild];
const streams = toStreams(children);
handleChildEvents(streams, children, childrenInfo);
}, config.restartAfter);
}
function killOtherProcesses(processes) {
logEvent('--> ', chalk.gray.dim, 'Sending SIGTERM to other processes..');
// Send SIGTERM to alive children
_.each(processes, function(child) {
treeKill(child.pid, 'SIGTERM');
});
}
function exit(childExitCodes) {
let success;
switch (config.success) {
case 'first':
success = _.first(childExitCodes) === 0;
break;
case 'last':
success = _.last(childExitCodes) === 0;
break;
default:
success = _.every(childExitCodes, function(code) {
return code === 0;
});
}
process.exit(success ? 0 : 1);
}
function handleError(streams, childrenInfo) {
// Output emitted errors from child process
const errorStreams = _.map(streams, 'error');
const processErrorStream = Rx.Observable.merge.apply(this, errorStreams);
processErrorStream.subscribe(function(event) {
const command = childrenInfo[event.child.pid].command;
logError('', chalk.gray.dim, 'Error occured when executing command: ' + command);
logError('', chalk.gray.dim, event.data.stack);
});
}
function colorText(text, color) {
if (!config.color) {
return text;
} else {
return color(text);
}
}
function getPrefix(childrenInfo, child) {
const prefixes = getPrefixes(childrenInfo, child);
const prefixType = config.prefix || prefixes.name ? 'name' : 'index';
if (_.includes(_.keys(prefixes), prefixType)) {
return '[' + prefixes[prefixType] + '] ';
}
return _.reduce(prefixes, function(memo, val, key) {
const re = new RegExp('{' + key + '}', 'g');
return memo.replace(re, val);
}, config.prefix) + ' ';
}
function getPrefixes(childrenInfo, child) {
const prefixes = {};
prefixes.none = '';
prefixes.pid = child.pid;
prefixes.index = childrenInfo[child.pid].index;
prefixes.name = childrenInfo[child.pid].name;
prefixes.time = formatDate(Date.now(), config.timestampFormat);
const command = childrenInfo[child.pid].command;
prefixes.command = shortenText(command, config.prefixLength);
return prefixes;
}
function shortenText(text, length, cut) {
if (text.length <= length) {
return text;
}
cut = _.isString(cut) ? cut : '..';
const endLength = Math.floor(length / 2);
const startLength = length - endLength;
const first = text.substring(0, startLength);
const last = text.substring(text.length - endLength, text.length);
return first + cut + last;
}
function log(prefix, prefixColor, text) {
logWithPrefix(prefix, prefixColor, text);
}
function logEvent(prefix, prefixColor, text) {
if (config.raw) {
return;
}
logWithPrefix(prefix, prefixColor, text + '\n', chalk.gray.dim);
}
function logError(prefix, prefixColor, text) {
// This is for now same as log, there might be separate colors for stderr
// and stdout
logWithPrefix(prefix, prefixColor, text, chalk.red.bold);
}
let lastChar;
function logWithPrefix(prefix, prefixColor, text, color) {
if (config.raw) {
process.stdout.write(text);
return;
}
text = text.replace(/\u2026/g,'...'); // Ellipsis
const lines = text.split('\n');
// Do not bgColor trailing space
const coloredPrefix = colorText(prefix.replace(/ $/, ''), prefixColor) + ' ';
const paddedLines = _.map(lines, function(line, index) {
let coloredLine = color ? colorText(line, color) : line;
if (index !== 0 && index !== (lines.length - 1)) {
coloredLine = coloredPrefix + coloredLine;
}
return coloredLine;
});
if (!lastChar || lastChar === '\n' ) {
process.stdout.write(coloredPrefix);
}
lastChar = text[text.length - 1];
process.stdout.write(paddedLines.join('\n'));
}
main();