From 6f8e674b33f061c7f63f9d9b688beb27032d6026 Mon Sep 17 00:00:00 2001 From: CzBuCHi Date: Wed, 28 Jun 2017 23:06:23 +0200 Subject: [PATCH 1/6] PR for Node API version? #101 --- CHANGELOG.md | 8 + package.json | 105 ++++---- src/api.js | 34 +++ src/lib.js | 370 ++++++++++++++++++++++++++++ src/main.js | 357 +-------------------------- test/support/sleep.js | 1 + test/test-functional.js | 533 +++++++++++++++++++++------------------- typings.d.ts | 54 ++++ 8 files changed, 803 insertions(+), 659 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/api.js create mode 100644 src/lib.js create mode 100644 test/support/sleep.js create mode 100644 typings.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..4b2b9c45 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +- these dont have effect on code (mocha failed on my windows machine) + - tests fix: windows doesnt have command 'sleep' - created one at './test/support/sleep.js' + - refactor: test-functional.js: 'Skipping SIGINT/SIGTERM propagation tests ...' is writen to console only once + +- extracted most of code to lib.js (main.js contains ony cli arguments parsing) +- moved 'exit' function in 'lib.js' to 'config' - otherwise it would kill process, who uses api +- added api.js with single exported function - 'run(commands, options)' +- added typings.d.ts for typescript support diff --git a/package.json b/package.json index cd466ee7..e8c911dd 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,55 @@ { - "name": "concurrently", - "version": "3.5.0", - "description": "Run commands concurrently", - "main": "src/main.js", - "bin": { - "concurrent": "./src/main.js", - "concurrently": "./src/main.js" - }, - "engines": { - "node": ">=4.0.0" - }, - "scripts": { + "name": "concurrently", + "version": "3.5.0", + "description": "Run commands concurrently", + "main": "src/api.js", + "typings": "typings.d.ts", + "bin": { + "concurrent": "./src/main.js", + "concurrently": "./src/main.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "scripts": { "test": "mocha" - }, - "repository": { - "type": "git", - "url": "https://github.com/kimmobrunfeldt/concurrently.git" - }, - "keywords": [ - "bash", - "concurrent", - "parallel", - "concurrently", - "command", - "sh" - ], - "author": "Kimmo Brunfeldt", - "license": "MIT", - "bugs": { - "url": "https://github.com/kimmobrunfeldt/concurrently/issues" - }, - "homepage": "https://github.com/kimmobrunfeldt/concurrently", - "dependencies": { - "chalk": "0.5.1", - "commander": "2.6.0", - "date-fns": "^1.23.0", - "lodash": "^4.5.1", - "rx": "2.3.24", - "spawn-command": "^0.0.2-1", - "supports-color": "^3.2.3", - "tree-kill": "^1.1.0" - }, - "devDependencies": { - "chai": "^1.10.0", - "mocha": "^2.1.0", - "mustache": "^1.0.0", - "releasor": "^1.2.1", - "semver": "^4.2.0", - "shell-quote": "^1.4.3", - "shelljs": "^0.3.0", - "string": "^3.0.0" - } -} + }, + "repository": { + "type": "git", + "url": "https://github.com/kimmobrunfeldt/concurrently.git" + }, + "keywords": [ + "bash", + "concurrent", + "parallel", + "concurrently", + "command", + "sh" + ], + "author": "Kimmo Brunfeldt", + "license": "MIT", + "bugs": { + "url": "https://github.com/kimmobrunfeldt/concurrently/issues" + }, + "homepage": "https://github.com/kimmobrunfeldt/concurrently", + "dependencies": { + "chalk": "0.5.1", + "commander": "2.6.0", + "date-fns": "^1.23.0", + "lodash": "^4.5.1", + "rx": "2.3.24", + "spawn-command": "^0.0.2-1", + "supports-color": "^3.2.3", + "tree-kill": "^1.1.0" + }, + "devDependencies": { + "chai": "^1.10.0", + "mocha": "^2.1.0", + "mustache": "^1.0.0", + "releasor": "^1.2.1", + "semver": "^4.2.0", + "shell-quote": "^1.4.3", + "shelljs": "^0.3.0", + "string": "^3.0.0" + } +} \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..c3ed7439 --- /dev/null +++ b/src/api.js @@ -0,0 +1,34 @@ +var _ = require('lodash'); + +var { config, run } = require('./lib.js'); + +module.exports = function(commands, options) { + return new Promise(resolve => { + const prefixColors = []; + const names = []; + const executables = []; + _.each(commands, cmd => { + if (typeof cmd === 'string') { + prefixColors.push(''); + names.push(''); + executables.push(cmd); + } else { + const cmdColors = _.filter([cmd.prefixModifier, cmd.prefixTextColor, cmd.prefixBackColor], o => o); + prefixColors.push(cmdColors.length > 0 ? _.join(cmdColors, '.') : ''); + names.push(cmd.name || ''); + executables.push(cmd.command); + } + }); + + _.assign(config, options, { + names: _.join(names, ','), + nameSeparator: ',', + prefixColors: _.join(prefixColors, ','), + exit: (childExitCodes) => { + resolve(childExitCodes); + } + }); + + run(executables); + }); +} \ No newline at end of file diff --git a/src/lib.js b/src/lib.js new file mode 100644 index 00000000..59ba6353 --- /dev/null +++ b/src/lib.js @@ -0,0 +1,370 @@ +#!/usr/bin/env node + +var Rx = require('rx'); +var path = require('path'); +var formatDate = require('date-fns/format'); +var _ = require('lodash'); +var treeKill = require('tree-kill'); +var chalk = require('chalk'); +var spawn = require('spawn-command'); +var supportsColor = require('supports-color'); +var IS_WINDOWS = /^win/.test(process.platform); + +var 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, + + // callback called when all children exits + exit: function(childExitCodes) { + var 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 stripCmdQuotes(cmd) { + // Removes the quotes surrounding a command. + if (cmd[0] === '"' || cmd[0] === '\'') { + return cmd.substr(1, cmd.length - 2); + } else { + return cmd; + } +} + +function run(commands) { + var childrenInfo = {}; + var lastPrefixColor = _.get(chalk, chalk.gray.dim); + var prefixColors = config.prefixColors.split(','); + var names = config.names.split(config.nameSeparator); + var children = _.map(commands, function(cmd, index) { + // Remove quotes. + cmd = stripCmdQuotes(cmd); + + var spawnOpts = config.raw ? { stdio: 'inherit' } : {}; + if (IS_WINDOWS) { + spawnOpts.detached = false; + } + if (supportsColor) { + spawnOpts.env = Object.assign({ FORCE_COLOR: supportsColor.level }, process.env) + } + + var child = spawnChild(cmd, spawnOpts); + + if (index < prefixColors.length) { + var prefixColorPath = prefixColors[index]; + lastPrefixColor = _.get(chalk, prefixColorPath, chalk.gray.dim); + } + + var name = index < names.length ? names[index] : ''; + childrenInfo[child.pid] = { + process: child, + command: cmd, + index: index, + name: name, + options: spawnOpts, + restartTries: config.restartTries, + prefixColor: lastPrefixColor + }; + return child; + }); + + var streams = toStreams(children); + + handleChildEvents(streams, children, childrenInfo); + + ['SIGINT', 'SIGTERM'].forEach(function(signal) { + process.on(signal, function() { + children.forEach(function(child) { + treeKill(child.pid, signal); + }); + }); + }); + + return childrenInfo; +} + +function spawnChild(cmd, options) { + var 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) { + var 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) { + var sourceStreams = _.map(streams, source); + var combinedSourceStream = Rx.Observable.merge.apply(this, sourceStreams); + + combinedSourceStream.subscribe(function(event) { + var prefix = getPrefix(childrenInfo, event.child); + var prefixColor = childrenInfo[event.child.pid].prefixColor; + log(prefix, prefixColor, event.data.toString()); + }); +} + +function handleClose(streams, children, childrenInfo) { + var allChildren = _.clone(children); + var aliveChildren = _.clone(children); + var exitCodes = []; + var closeStreams = _.map(streams, 'close'); + var closeStream = Rx.Observable.merge.apply(this, closeStreams); + var othersKilled = false + + // TODO: Is it possible that amount of close events !== count of spawned? + closeStream.subscribe(function(event) { + var exitCode = event.data; + var nonSuccess = exitCode !== 0; + exitCodes[allChildren.indexOf(event.child)] = exitCode; + + var prefix = getPrefix(childrenInfo, event.child); + var childInfo = childrenInfo[event.child.pid]; + var prefixColor = childInfo.prefixColor; + var 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) { + config.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() { + var childInfo = childrenInfo[event.child.pid]; + var prefix = getPrefix(childrenInfo, event.child); + var prefixColor = childInfo.prefixColor; + logEvent(prefix, prefixColor, childInfo.command + ' restarted'); + var newChild = spawnChild(childInfo.command, childInfo.options); + + childrenInfo[newChild.pid] = childrenInfo[event.child.pid]; + delete childrenInfo[event.child.pid]; + + var children = [newChild]; + var 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 handleError(streams, childrenInfo) { + // Output emitted errors from child process + var errorStreams = _.map(streams, 'error'); + var processErrorStream = Rx.Observable.merge.apply(this, errorStreams); + + processErrorStream.subscribe(function(event) { + var 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) { + var prefixes = getPrefixes(childrenInfo, child); + if (_.includes(_.keys(prefixes), config.prefix)) { + return '[' + prefixes[config.prefix] + '] '; + } + + return _.reduce(prefixes, function(memo, val, key) { + var re = new RegExp('{' + key + '}', 'g'); + return memo.replace(re, val); + }, config.prefix) + ' '; +} + +function getPrefixes(childrenInfo, child) { + var 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); + + var 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 :  '..'; + + var endLength = Math.floor(length / 2); + var startLength = length - endLength; + + var first = text.substring(0, startLength); + var 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, 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); +} + +function logWithPrefix(prefix, prefixColor, text, color) { + var lastChar = text[text.length - 1]; + if (config.raw) { + if (lastChar !== '\n') { + text += '\n'; + } + + process.stdout.write(text); + return; + } + + if (lastChar === '\n') { + // Remove extra newline from the end to prevent extra newlines in input + text = text.slice(0, text.length - 1); + } + + var lines = text.split('\n'); + // Do not bgColor trailing space + var coloredPrefix = colorText(prefix.replace(/ $/, ''), prefixColor) + ' '; + var paddedLines = _.map(lines, function(line, i) { + var coloredLine = color ? colorText(line, color) : line; + return coloredPrefix + coloredLine; + }); + + console.log(paddedLines.join('\n')); +} + +module.exports = { + config, + run +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 4164eb4c..dae5fbdf 100755 --- a/src/main.js +++ b/src/main.js @@ -1,62 +1,10 @@ #!/usr/bin/env node -var Rx = require('rx'); var path = require('path'); -var formatDate = require('date-fns/format'); var program = require('commander'); var _ = require('lodash'); -var treeKill = require('tree-kill'); -var chalk = require('chalk'); -var spawn = require('spawn-command'); -var supportsColor = require('supports-color'); -var IS_WINDOWS = /^win/.test(process.platform); -var 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 -}; +var { config, run } = require('./lib.js'); function main() { var firstBase = path.basename(process.argv[0]); @@ -204,305 +152,4 @@ function applyDynamicDefaults(config) { } } -function stripCmdQuotes(cmd) { - // Removes the quotes surrounding a command. - if (cmd[0] === '"' || cmd[0] === '\'') { - return cmd.substr(1, cmd.length - 2); - } else { - return cmd; - } -} - -function run(commands) { - var childrenInfo = {}; - var lastPrefixColor = _.get(chalk, chalk.gray.dim); - var prefixColors = config.prefixColors.split(','); - var names = config.names.split(config.nameSeparator); - var children = _.map(commands, function(cmd, index) { - // Remove quotes. - cmd = stripCmdQuotes(cmd); - - var spawnOpts = config.raw ? {stdio: 'inherit'} : {}; - if (IS_WINDOWS) { - spawnOpts.detached = false; - } - if (supportsColor) { - spawnOpts.env = Object.assign({FORCE_COLOR: supportsColor.level}, process.env) - } - - var child = spawnChild(cmd, spawnOpts); - - if (index < prefixColors.length) { - var prefixColorPath = prefixColors[index]; - lastPrefixColor = _.get(chalk, prefixColorPath, chalk.gray.dim); - } - - var name = index < names.length ? names[index] : ''; - childrenInfo[child.pid] = { - command: cmd, - index: index, - name: name, - options: spawnOpts, - restartTries: config.restartTries, - prefixColor: lastPrefixColor - }; - return child; - }); - - var streams = toStreams(children); - - handleChildEvents(streams, children, childrenInfo); - - ['SIGINT', 'SIGTERM'].forEach(function(signal) { - process.on(signal, function() { - children.forEach(function(child) { - treeKill(child.pid, signal); - }); - }); - }); -} - -function spawnChild(cmd, options) { - var 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) { - var 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) { - var sourceStreams = _.map(streams, source); - var combinedSourceStream = Rx.Observable.merge.apply(this, sourceStreams); - - combinedSourceStream.subscribe(function(event) { - var prefix = getPrefix(childrenInfo, event.child); - var prefixColor = childrenInfo[event.child.pid].prefixColor; - log(prefix, prefixColor, event.data.toString()); - }); -} - -function handleClose(streams, children, childrenInfo) { - var aliveChildren = _.clone(children); - var exitCodes = []; - var closeStreams = _.map(streams, 'close'); - var closeStream = Rx.Observable.merge.apply(this, closeStreams); - var othersKilled = false - - // TODO: Is it possible that amount of close events !== count of spawned? - closeStream.subscribe(function(event) { - var exitCode = event.data; - var nonSuccess = exitCode !== 0; - exitCodes.push(exitCode); - - var prefix = getPrefix(childrenInfo, event.child); - var childInfo = childrenInfo[event.child.pid]; - var prefixColor = childInfo.prefixColor; - var 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() { - var childInfo = childrenInfo[event.child.pid]; - var prefix = getPrefix(childrenInfo, event.child); - var prefixColor = childInfo.prefixColor; - logEvent(prefix, prefixColor, childInfo.command + ' restarted'); - var newChild = spawnChild(childInfo.command, childInfo.options); - - childrenInfo[newChild.pid] = childrenInfo[event.child.pid]; - delete childrenInfo[event.child.pid]; - - var children = [newChild]; - var 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) { - var 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 - var errorStreams = _.map(streams, 'error'); - var processErrorStream = Rx.Observable.merge.apply(this, errorStreams); - - processErrorStream.subscribe(function(event) { - var 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) { - var prefixes = getPrefixes(childrenInfo, child); - if (_.includes(_.keys(prefixes), config.prefix)) { - return '[' + prefixes[config.prefix] + '] '; - } - - return _.reduce(prefixes, function(memo, val, key) { - var re = new RegExp('{' + key + '}', 'g'); - return memo.replace(re, val); - }, config.prefix) + ' '; -} - -function getPrefixes(childrenInfo, child) { - var 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); - - var 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 : '..'; - - var endLength = Math.floor(length / 2); - var startLength = length - endLength; - - var first = text.substring(0, startLength); - var 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, 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); -} - -function logWithPrefix(prefix, prefixColor, text, color) { - var lastChar = text[text.length - 1]; - if (config.raw) { - if (lastChar !== '\n') { - text += '\n'; - } - - process.stdout.write(text); - return; - } - - if (lastChar === '\n') { - // Remove extra newline from the end to prevent extra newlines in input - text = text.slice(0, text.length - 1); - } - - var lines = text.split('\n'); - // Do not bgColor trailing space - var coloredPrefix = colorText(prefix.replace(/ $/, ''), prefixColor) + ' '; - var paddedLines = _.map(lines, function(line, i) { - var coloredLine = color ? colorText(line, color) : line; - return coloredPrefix + coloredLine; - }); - - console.log(paddedLines.join('\n')); -} - -main(); +main(); \ No newline at end of file diff --git a/test/support/sleep.js b/test/support/sleep.js new file mode 100644 index 00000000..d6147bd9 --- /dev/null +++ b/test/support/sleep.js @@ -0,0 +1 @@ +new Promise(resolve => setTimeout(resolve, parseInt(process.argv[2], 10) * 1000)).then(() => process.exit(0)); \ No newline at end of file diff --git a/test/test-functional.js b/test/test-functional.js index 204dfc3d..09ca0346 100644 --- a/test/test-functional.js +++ b/test/test-functional.js @@ -4,6 +4,7 @@ var path = require('path'); var assert = require('assert'); var run = require('./utils').run; var IS_WINDOWS = /^win/.test(process.platform); +var concurrently = require('../src/api.js'); // Note: Set the DEBUG_TESTS environment variable to `true` to see output of test commands. @@ -15,262 +16,290 @@ process.chdir(path.join(testDir, '..')); describe('concurrently', function() { this.timeout(5000); - - it('help should be successful', () => { - return run('node ./src/main.js --help') - .then(function(exitCode) { - // exit code 0 means success - assert.strictEqual(exitCode, 0); - }); - }); - - it('version should be successful', () => { - return run('node ./src/main.js -V') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('two successful commands should exit 0', () => { - return run('node ./src/main.js "echo test" "echo test"') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('at least one unsuccessful commands should exit non-zero', () => { - return run('node ./src/main.js "echo test" "nosuchcmd" "echo test"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others should kill other commands if one dies', () => { - return run('node ./src/main.js --kill-others "sleep 1" "echo test" "sleep 0.1 && nosuchcmd"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others-on-fail should kill other commands if one exits with non-zero status code', () => { - return run('node ./src/main.js --kill-others-on-fail "sleep 1" "exit 1" "sleep 1"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others-on-fail should NOT kill other commands if none of them exits with non-zero status code', (done) => { - var readline = require('readline'); - var exits = 0; - var sigtermInOutput = false; - - run('node ./src/main.js --kill-others-on-fail "echo killTest1" "echo killTest2" "echo killTest3"', { - onOutputLine: function(line) { - if (/SIGTERM/.test(line)) { - sigtermInOutput = true; - } - - // waiting for exits - if (/killTest\d$/.test(line)) { - exits++; - } - } - }).then(function() { - if(sigtermInOutput) { - done(new Error('There was a "SIGTERM" in console output')); - } else if (exits !== 3) { - done(new Error('There was wrong number of echoes(' + exits + ') from executed commands')); - } else { - done(); - } + + it('help should be successful', () => { + return run('node ./src/main.js --help') + .then(function(exitCode) { + // exit code 0 means success + assert.strictEqual(exitCode, 0); + }); + }); + + it('version should be successful', () => { + return run('node ./src/main.js -V') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('two successful commands should exit 0', () => { + return run('node ./src/main.js "echo test" "echo test"') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('at least one unsuccessful commands should exit non-zero', () => { + return run('node ./src/main.js "echo test" "nosuchcmd" "echo test"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others should kill other commands if one dies', () => { + return run('node ./src/main.js --kill-others "node ./test/support/sleep.js 1" "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others-on-fail should kill other commands if one exits with non-zero status code', () => { + return run('node ./src/main.js --kill-others-on-fail "node ./test/support/sleep.js 1" "exit 1" "node ./test/support/sleep.js 1"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others-on-fail should NOT kill other commands if none of them exits with non-zero status code', (done) => { + var readline = require('readline'); + var exits = 0; + var sigtermInOutput = false; + + run('node ./src/main.js --kill-others-on-fail "echo killTest1" "echo killTest2" "echo killTest3"', { + onOutputLine: function(line) { + if (/SIGTERM/.test(line)) { + sigtermInOutput = true; + } + + // waiting for exits + if (/killTest\d$/.test(line)) { + exits++; + } + } + }).then(function() { + if (sigtermInOutput) { + done(new Error('There was a "SIGTERM" in console output')); + } else if (exits !== 3) { + done(new Error('There was wrong number of echoes(' + exits + ') from executed commands')); + } else { + done(); + } + }); + }); + + it('--success=first should return first exit code', () => { + return run('node ./src/main.js -k --success first "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + // When killed, sleep returns null exit code + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('--success=last should return last exit code', () => { + // When killed, sleep returns null exit code + return run('node ./src/main.js -k --success last "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('&& nosuchcmd should return non-zero exit code', () => { + return run('node ./src/main.js "echo 1 && nosuchcmd" "echo 1 && nosuchcmd" ') + .then(function(exitCode) { + assert.strictEqual(exitCode, 1); + }); + }); + + it('--prefix-colors should handle non-existent colors without failing', () => { + return run('node ./src/main.js -c "not.a.color" "echo colors"') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('--prefix should default to "index"', () => { + var collectedLines = [] + + return run('node ./src/main.js "echo one" "echo two"', { + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } + } + }) + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + + collectedLines.sort() + assert.deepEqual(collectedLines, [ + '[0] one', + '[1] two' + ]) + }); + }); + + it('--names should set a different default prefix', () => { + var collectedLines = [] + + return run('node ./src/main.js -n aa,bb "echo one" "echo two"', { + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } + } + }) + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + + collectedLines.sort() + assert.deepEqual(collectedLines, [ + '[aa] one', + '[bb] two' + ]) + }); + }); + + it('--allow-restart should restart a proccess with non-zero exit code', (done) => { + var readline = require('readline'); + var exitedWithOne = false; + var restarted = false; + + run('node ./src/main.js --allow-restart "node ./test/support/sleep.js 0.1 && exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + var re = /exited with code (.+)/.exec(line); + if (re && re[1] === '1') { + exitedWithOne = true + } + + if (/restarted/.test(line)) { + restarted = true; + } + } + }).then(function() { + if (exitedWithOne && restarted) { + done(); + } else { + done(new Error('No restarted process exited with code 1')); + } + }); + }); + + it('--restart-after=n should restart a proccess after n miliseconds', (done) => { + var readline = require('readline'); + var start, end; + + run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + if (!start && /exited with code (.+)/.test(line)) { + start = new Date().getTime(); + } + + if (!end && /restarted/.test(line)) { + end = new Date().getTime(); + } + } + }).then(function() { + // we accept 100 miliseconds of error + if (end - start >= 300 && end - start < 400) { + done(); + } else { + done(new Error('No restarted process after 300 miliseconds - delta is: ' + (end - start))); + } + }); + }); + + it('--restart-tries=n should restart a proccess at most n times', (done) => { + var readline = require('readline'); + var restartedTimes = 0; + + run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + if (/restarted/.test(line)) { + restartedTimes++; + } + } + }).then(function() { + if (restartedTimes == 2) { + done(); + } else { + done(new Error('No restarted process twice')); + } + }); + }); + + if (IS_WINDOWS) { + console.log('IS_WINDOWS=true'); + console.log('Skipping SIGINT/SIGTERM propagation tests ...'); + } else { + ['SIGINT', 'SIGTERM'].forEach((signal) => { + it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { + var readline = require('readline'); + var waitingStart = 2; + var waitingSignal = 2; + + function waitForSignal(cb) { + if (waitingSignal) { + setTimeout(waitForSignal, 100); + } else { + cb(); + } + } + + run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { + onOutputLine: function(line, child) { + // waiting for startup + if (/STARTED/.test(line)) { + waitingStart--; + } + if (!waitingStart) { + // both processes are started + child.kill(signal); + } + + // waiting for signal + if (new RegExp(signal).test(line)) { + waitingSignal--; + } + } + }).then(function() { + waitForSignal(done); + }); + }); + }); + } + + describe('api', () => { + it('should run and return correct exit codes', () => { + var commands = ['echo 1', 'exit 2']; + return concurrently(commands) + .then(function(result) { + assert(result.length === 2); + assert(result[0] === 0); + assert(result[1] === 2); + }); }); - }); - - it('--success=first should return first exit code', () => { - return run('node ./src/main.js -k --success first "echo test" "sleep 0.1 && nosuchcmd"') - // When killed, sleep returns null exit code - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('--success=last should return last exit code', () => { - // When killed, sleep returns null exit code - return run('node ./src/main.js -k --success last "echo test" "sleep 0.1 && nosuchcmd"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('&& nosuchcmd should return non-zero exit code', () => { - return run('node ./src/main.js "echo 1 && nosuchcmd" "echo 1 && nosuchcmd" ') - .then(function(exitCode) { - assert.strictEqual(exitCode, 1); - }); - }); - - it('--prefix-colors should handle non-existent colors without failing', () => { - return run('node ./src/main.js -c "not.a.color" "echo colors"') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('--prefix should default to "index"', () => { - var collectedLines = [] - - return run('node ./src/main.js "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) - } - } - }) - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - - collectedLines.sort() - assert.deepEqual(collectedLines, [ - '[0] one', - '[1] two' - ]) - }); - }); - - it('--names should set a different default prefix', () => { - var collectedLines = [] - - return run('node ./src/main.js -n aa,bb "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) - } - } - }) - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - - collectedLines.sort() - assert.deepEqual(collectedLines, [ - '[aa] one', - '[bb] two' - ]) - }); - }); - - it('--allow-restart should restart a proccess with non-zero exit code', (done) => { - var readline = require('readline'); - var exitedWithOne = false; - var restarted = false; - - run('node ./src/main.js --allow-restart "sleep 0.1 && exit 1" "sleep 1"', { - pipe: false, - onOutputLine: (line) => { - var re = /exited with code (.+)/.exec(line); - if (re && re[1] === '1') { - exitedWithOne = true - } - - if (/restarted/.test(line)) { - restarted = true; - } - } - }).then(function() { - if (exitedWithOne && restarted) { - done(); - } else { - done(new Error('No restarted process exited with code 1')); - } - }); - }); - - it('--restart-after=n should restart a proccess after n miliseconds', (done) => { - var readline = require('readline'); - var start, end; - - run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "sleep 1"', { - pipe: false, - onOutputLine: (line) => { - if (!start && /exited with code (.+)/.test(line)) { - start = new Date().getTime(); - } - - if (!end && /restarted/.test(line)) { - end = new Date().getTime(); - } - } - }).then(function() { - // we accept 100 miliseconds of error - if (end - start >= 300 && end - start < 400) { - done(); - } else { - done(new Error('No restarted process after 300 miliseconds')); - } - }); - }); - it('--restart-tries=n should restart a proccess at most n times', (done) => { - var readline = require('readline'); - var restartedTimes = 0; - - run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "sleep 1"', { - pipe: false, - onOutputLine: (line) => { - if (/restarted/.test(line)) { - restartedTimes++; - } - } - }).then(function() { - if (restartedTimes == 2) { - done(); - } else { - done(new Error('No restarted process twice')); - } - }); - }); - - ['SIGINT', 'SIGTERM'].forEach((signal) => { - if (IS_WINDOWS) { - console.log('IS_WINDOWS=true'); - console.log('Skipping SIGINT/SIGTERM propagation tests ..'); - return; - } - - it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { - var readline = require('readline'); - var waitingStart = 2; - var waitingSignal = 2; - - function waitForSignal(cb) { - if (waitingSignal) { - setTimeout(waitForSignal, 100); - } else { - cb(); - } - } - - run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { - onOutputLine: function(line, child) { - // waiting for startup - if (/STARTED/.test(line)) { - waitingStart--; - } - if (!waitingStart) { - // both processes are started - child.kill(signal); - } - - // waiting for signal - if (new RegExp(signal).test(line)) { - waitingSignal--; - } - } - }).then(function() { - waitForSignal(done); + it('should use names', () => { + // not sure how to test colors ... + var commands = [{ + name: 'echo', + prefixModifier: 'dim', + prefixTextColor: 'yellow', + prefixBackColor: 'bgGreen', + command: 'echo 1' + }, 'exit 2']; + return concurrently(commands) + .then(function(result) { + assert(result.length === 2); + assert(result[0] === 0); + assert(result[1] === 2); + }); }); - }); }); }); function resolve(relativePath) { return path.join(testDir, relativePath); -} +} \ No newline at end of file diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 00000000..994f3721 --- /dev/null +++ b/typings.d.ts @@ -0,0 +1,54 @@ +declare module "concurrently" { + + export interface Options { + // Kill other processes if one dies + killOthers?: boolean, + + // Kill other processes if one exits with non zero status code + killOthersOnFail?: boolean, + + // Return success or failure of the 'first' child to terminate, the 'last' child, + // or succeed only if 'all' children succeed + success?: 'first' | 'last' | 'all', + + // Prefix logging with pid + prefix?: '' | 'pid' | 'none' | 'time' | 'command' | 'index' | 'name', + + // moment/date-fns format + timestampFormat?: string, + + // 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?: number, + + // By default, color output + color?: boolean, + + // If true, the output will only be raw output of processes, nothing more + raw?: boolean, + + // If true, the process restart when it exited with status code non-zero + allowRestart?: boolean, + + // By default, restart instantly + restartAfter?: number, + + // By default, restart once + restartTries?: number + } + + // see https://github.com/chalk/chalk#styles for details + export type modifier = 'reset' | 'bold' | 'dim' | 'italic' | 'underline' | 'inverse' | 'hidden' | 'strikethrough'; + export type textColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | 'blackBright' | 'redBright' | 'greenBright' | 'yellowBright' | 'blueBright' | 'magentaBright' | 'cyanBright' | 'whiteBright'; + export type backColor = 'bgBlack' | 'bgRed' | 'bgGreen' | 'bgYellow' | 'bgBlue' | 'bgMagenta' | 'bgCyan' | 'bgWhite' | 'bgBlackBright' | 'bgRedBright' | 'bgGreenBright' | 'bgYellowBright' | 'bgBlueBright' | 'bgMagentaBright' | 'bgCyanBright' | 'bgWhiteBright'; + + export interface Command { + name?: string, + prefixModifier?: modifier, + prefixTextColor?: textColor, + prefixBackColor?: backColor, + command: string, + } + + export function run(commands: (string | Command)[], options?: Options): Promise; +} From 2eb8e1b88a34eed98da2c8b1b1d8780d6bdc3cd2 Mon Sep 17 00:00:00 2001 From: CzBuCHi Date: Wed, 28 Jun 2017 23:15:01 +0200 Subject: [PATCH 2/6] indent fix --- package.json | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index e8c911dd..d47b9de1 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,55 @@ { - "name": "concurrently", - "version": "3.5.0", - "description": "Run commands concurrently", - "main": "src/api.js", - "typings": "typings.d.ts", - "bin": { - "concurrent": "./src/main.js", - "concurrently": "./src/main.js" - }, - "engines": { - "node": ">=4.0.0" - }, - "scripts": { + "name": "concurrently", + "version": "3.5.0", + "description": "Run commands concurrently", + "main": "src/main.js", + "typings": "typings.d.ts", + "bin": { + "concurrent": "./src/main.js", + "concurrently": "./src/main.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "scripts": { "test": "mocha" - }, - "repository": { - "type": "git", - "url": "https://github.com/kimmobrunfeldt/concurrently.git" - }, - "keywords": [ - "bash", - "concurrent", - "parallel", - "concurrently", - "command", - "sh" - ], - "author": "Kimmo Brunfeldt", - "license": "MIT", - "bugs": { - "url": "https://github.com/kimmobrunfeldt/concurrently/issues" - }, - "homepage": "https://github.com/kimmobrunfeldt/concurrently", - "dependencies": { - "chalk": "0.5.1", - "commander": "2.6.0", - "date-fns": "^1.23.0", - "lodash": "^4.5.1", - "rx": "2.3.24", - "spawn-command": "^0.0.2-1", - "supports-color": "^3.2.3", - "tree-kill": "^1.1.0" - }, - "devDependencies": { - "chai": "^1.10.0", - "mocha": "^2.1.0", - "mustache": "^1.0.0", - "releasor": "^1.2.1", - "semver": "^4.2.0", - "shell-quote": "^1.4.3", - "shelljs": "^0.3.0", - "string": "^3.0.0" - } -} \ No newline at end of file + }, + "repository": { + "type": "git", + "url": "https://github.com/kimmobrunfeldt/concurrently.git" + }, + "keywords": [ + "bash", + "concurrent", + "parallel", + "concurrently", + "command", + "sh" + ], + "author": "Kimmo Brunfeldt", + "license": "MIT", + "bugs": { + "url": "https://github.com/kimmobrunfeldt/concurrently/issues" + }, + "homepage": "https://github.com/kimmobrunfeldt/concurrently", + "dependencies": { + "chalk": "0.5.1", + "commander": "2.6.0", + "date-fns": "^1.23.0", + "lodash": "^4.5.1", + "rx": "2.3.24", + "spawn-command": "^0.0.2-1", + "supports-color": "^3.2.3", + "tree-kill": "^1.1.0" + }, + "devDependencies": { + "chai": "^1.10.0", + "mocha": "^2.1.0", + "mustache": "^1.0.0", + "releasor": "^1.2.1", + "semver": "^4.2.0", + "shell-quote": "^1.4.3", + "shelljs": "^0.3.0", + "string": "^3.0.0" + } +} From 80edfdda1e70a7650f3ae4919725a030abe20016 Mon Sep 17 00:00:00 2001 From: CzBuCHi Date: Wed, 28 Jun 2017 23:17:13 +0200 Subject: [PATCH 3/6] changelog --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 4b2b9c45..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -- these dont have effect on code (mocha failed on my windows machine) - - tests fix: windows doesnt have command 'sleep' - created one at './test/support/sleep.js' - - refactor: test-functional.js: 'Skipping SIGINT/SIGTERM propagation tests ...' is writen to console only once - -- extracted most of code to lib.js (main.js contains ony cli arguments parsing) -- moved 'exit' function in 'lib.js' to 'config' - otherwise it would kill process, who uses api -- added api.js with single exported function - 'run(commands, options)' -- added typings.d.ts for typescript support From e4b3d80be0de4ee9e8850927954560e8608bf295 Mon Sep 17 00:00:00 2001 From: CzBuCHi Date: Wed, 28 Jun 2017 23:24:58 +0200 Subject: [PATCH 4/6] cleanup --- src/lib.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lib.js b/src/lib.js index 59ba6353..eb486829 100644 --- a/src/lib.js +++ b/src/lib.js @@ -111,7 +111,6 @@ function run(commands) { var name = index < names.length ? names[index] : ''; childrenInfo[child.pid] = { - process: child, command: cmd, index: index, name: name, @@ -133,8 +132,6 @@ function run(commands) { }); }); }); - - return childrenInfo; } function spawnChild(cmd, options) { From 76b76ecaddcb583b1e38ae34cf80dfe8dccaf40b Mon Sep 17 00:00:00 2001 From: Marek Date: Thu, 29 Jun 2017 08:43:41 +0200 Subject: [PATCH 5/6] fixed integration errors --- src/api.js | 7 +++---- src/lib.js | 2 -- src/main.js | 5 +++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/api.js b/src/api.js index c3ed7439..306d196a 100644 --- a/src/api.js +++ b/src/api.js @@ -1,6 +1,5 @@ var _ = require('lodash'); - -var { config, run } = require('./lib.js'); +var lib = require('./lib.js'); module.exports = function(commands, options) { return new Promise(resolve => { @@ -20,7 +19,7 @@ module.exports = function(commands, options) { } }); - _.assign(config, options, { + _.assign(lib.config, options, { names: _.join(names, ','), nameSeparator: ',', prefixColors: _.join(prefixColors, ','), @@ -29,6 +28,6 @@ module.exports = function(commands, options) { } }); - run(executables); + lib.run(executables); }); } \ No newline at end of file diff --git a/src/lib.js b/src/lib.js index eb486829..af1cbef5 100644 --- a/src/lib.js +++ b/src/lib.js @@ -1,5 +1,3 @@ -#!/usr/bin/env node - var Rx = require('rx'); var path = require('path'); var formatDate = require('date-fns/format'); diff --git a/src/main.js b/src/main.js index dae5fbdf..7a2937a0 100755 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,8 @@ var path = require('path'); var program = require('commander'); var _ = require('lodash'); -var { config, run } = require('./lib.js'); +var lib = require('./lib.js'); +var config = lib.config; function main() { var firstBase = path.basename(process.argv[0]); @@ -17,7 +18,7 @@ function main() { config = mergeDefaultsWithArgs(config); applyDynamicDefaults(config) - run(program.args); + lib.run(program.args); } function parseArgs() { From fbdcf0ddc497067127ba789628838c353df42e90 Mon Sep 17 00:00:00 2001 From: Marek Date: Thu, 29 Jun 2017 10:05:03 +0200 Subject: [PATCH 6/6] prefixes display fix --- package.json | 2 +- src/api.js | 45 ++-- src/lib.js | 74 +++--- src/main.js | 16 +- test/test-functional.js | 511 ++++++++++++++++++++-------------------- 5 files changed, 321 insertions(+), 327 deletions(-) diff --git a/package.json b/package.json index d47b9de1..13704180 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "concurrently", "version": "3.5.0", "description": "Run commands concurrently", - "main": "src/main.js", + "main": "src/api.js", "typings": "typings.d.ts", "bin": { "concurrent": "./src/main.js", diff --git a/src/api.js b/src/api.js index 306d196a..d0a87b4b 100644 --- a/src/api.js +++ b/src/api.js @@ -2,32 +2,27 @@ var _ = require('lodash'); var lib = require('./lib.js'); module.exports = function(commands, options) { - return new Promise(resolve => { - const prefixColors = []; - const names = []; - const executables = []; - _.each(commands, cmd => { - if (typeof cmd === 'string') { - prefixColors.push(''); - names.push(''); - executables.push(cmd); - } else { - const cmdColors = _.filter([cmd.prefixModifier, cmd.prefixTextColor, cmd.prefixBackColor], o => o); - prefixColors.push(cmdColors.length > 0 ? _.join(cmdColors, '.') : ''); - names.push(cmd.name || ''); - executables.push(cmd.command); - } - }); + const prefixColors = []; + const names = []; + const executables = []; + _.each(commands, cmd => { + if (typeof cmd === 'string') { + prefixColors.push(''); + names.push(''); + executables.push(cmd); + } else { + const cmdColors = _.filter([cmd.prefixModifier, cmd.prefixTextColor, cmd.prefixBackColor], o => o); + prefixColors.push(cmdColors.length > 0 ? _.join(cmdColors, '.') : ''); + names.push(cmd.name || ''); + executables.push(cmd.command); + } + }); - _.assign(lib.config, options, { - names: _.join(names, ','), - nameSeparator: ',', - prefixColors: _.join(prefixColors, ','), - exit: (childExitCodes) => { - resolve(childExitCodes); - } - }); + _.assign(lib.config, options); - lib.run(executables); + return new Promise(resolve => { + lib.run(executables, names, prefixColors, childExitCodes => { + resolve(childExitCodes); + }); }); } \ No newline at end of file diff --git a/src/lib.js b/src/lib.js index af1cbef5..39b8e6bc 100644 --- a/src/lib.js +++ b/src/lib.js @@ -52,26 +52,7 @@ var config = { restartAfter: 0, // By default, restart once - restartTries: 1, - - // callback called when all children exits - exit: function(childExitCodes) { - var 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); - } + restartTries: 1 }; function stripCmdQuotes(cmd) { @@ -83,11 +64,13 @@ function stripCmdQuotes(cmd) { } } -function run(commands) { +function run(commands, names, prefixColors, exitCallback) { + if (!config.prefix) { + config.prefix = names ? 'name' : 'index'; + } + var childrenInfo = {}; var lastPrefixColor = _.get(chalk, chalk.gray.dim); - var prefixColors = config.prefixColors.split(','); - var names = config.names.split(config.nameSeparator); var children = _.map(commands, function(cmd, index) { // Remove quotes. cmd = stripCmdQuotes(cmd); @@ -121,7 +104,7 @@ function run(commands) { var streams = toStreams(children); - handleChildEvents(streams, children, childrenInfo); + handleChildEvents(streams, children, childrenInfo, exitCallback); ['SIGINT', 'SIGTERM'].forEach(function(signal) { process.on(signal, function() { @@ -132,6 +115,15 @@ function run(commands) { }); } +function runCommands(commands) { + var prefixColors = config.prefixColors.split(','); + var names = config.names.split(config.nameSeparator); + if (!config.prefix) { + config.prefix = config.names ? 'name' : 'index'; + } + run(commands, names, prefixColors, exit); +} + function spawnChild(cmd, options) { var child; try { @@ -166,8 +158,8 @@ function toStreams(children) { }); } -function handleChildEvents(streams, children, childrenInfo) { - handleClose(streams, children, childrenInfo); +function handleChildEvents(streams, children, childrenInfo, exitCallback) { + handleClose(streams, children, childrenInfo, exitCallback); handleError(streams, childrenInfo); if (!config.raw) { handleOutput(streams, childrenInfo, 'stdout'); @@ -186,7 +178,7 @@ function handleOutput(streams, childrenInfo, source) { }); } -function handleClose(streams, children, childrenInfo) { +function handleClose(streams, children, childrenInfo, exitCallback) { var allChildren = _.clone(children); var aliveChildren = _.clone(children); var exitCodes = []; @@ -211,12 +203,12 @@ function handleClose(streams, children, childrenInfo) { }); if (nonSuccess && config.allowRestart && childInfo.restartTries--) { - respawnChild(event, childrenInfo); + respawnChild(event, childrenInfo, exitCallback); return; } if (aliveChildren.length === 0) { - config.exit(exitCodes); + exitCallback(exitCodes); } if (!othersKilled) { if (config.killOthers) { @@ -230,7 +222,7 @@ function handleClose(streams, children, childrenInfo) { }); } -function respawnChild(event, childrenInfo) { +function respawnChild(event, childrenInfo, exitCallback) { setTimeout(function() { var childInfo = childrenInfo[event.child.pid]; var prefix = getPrefix(childrenInfo, event.child); @@ -243,7 +235,7 @@ function respawnChild(event, childrenInfo) { var children = [newChild]; var streams = toStreams(children); - handleChildEvents(streams, children, childrenInfo); + handleChildEvents(streams, children, childrenInfo, exitCallback); }, config.restartAfter); } @@ -256,6 +248,23 @@ function killOtherProcesses(processes) { }); } +function exit(childExitCodes) { + var 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 var errorStreams = _.map(streams, 'error'); @@ -361,5 +370,6 @@ function logWithPrefix(prefix, prefixColor, text, color) { module.exports = { config, - run + run, + runCommands } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 7a2937a0..b154cd94 100755 --- a/src/main.js +++ b/src/main.js @@ -15,10 +15,9 @@ function main() { } parseArgs(); - config = mergeDefaultsWithArgs(config); - applyDynamicDefaults(config) + config = _.merge(config, program); - lib.run(program.args); + lib.runCommands(program.args); } function parseArgs() { @@ -142,15 +141,4 @@ function parseArgs() { program.parse(process.argv); } -function mergeDefaultsWithArgs(config) { - // This will pollute config object with other attributes from program too - return _.merge(config, program); -} - -function applyDynamicDefaults(config) { - if (!config.prefix) { - config.prefix = config.names ? 'name' : 'index'; - } -} - main(); \ No newline at end of file diff --git a/test/test-functional.js b/test/test-functional.js index 09ca0346..f1fea4e6 100644 --- a/test/test-functional.js +++ b/test/test-functional.js @@ -16,262 +16,263 @@ process.chdir(path.join(testDir, '..')); describe('concurrently', function() { this.timeout(5000); - - it('help should be successful', () => { - return run('node ./src/main.js --help') - .then(function(exitCode) { - // exit code 0 means success - assert.strictEqual(exitCode, 0); - }); - }); - - it('version should be successful', () => { - return run('node ./src/main.js -V') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('two successful commands should exit 0', () => { - return run('node ./src/main.js "echo test" "echo test"') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('at least one unsuccessful commands should exit non-zero', () => { - return run('node ./src/main.js "echo test" "nosuchcmd" "echo test"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others should kill other commands if one dies', () => { - return run('node ./src/main.js --kill-others "node ./test/support/sleep.js 1" "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others-on-fail should kill other commands if one exits with non-zero status code', () => { - return run('node ./src/main.js --kill-others-on-fail "node ./test/support/sleep.js 1" "exit 1" "node ./test/support/sleep.js 1"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('--kill-others-on-fail should NOT kill other commands if none of them exits with non-zero status code', (done) => { - var readline = require('readline'); - var exits = 0; - var sigtermInOutput = false; - - run('node ./src/main.js --kill-others-on-fail "echo killTest1" "echo killTest2" "echo killTest3"', { - onOutputLine: function(line) { - if (/SIGTERM/.test(line)) { - sigtermInOutput = true; - } - - // waiting for exits - if (/killTest\d$/.test(line)) { - exits++; - } - } - }).then(function() { - if (sigtermInOutput) { - done(new Error('There was a "SIGTERM" in console output')); - } else if (exits !== 3) { - done(new Error('There was wrong number of echoes(' + exits + ') from executed commands')); - } else { - done(); - } - }); - }); - - it('--success=first should return first exit code', () => { - return run('node ./src/main.js -k --success first "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') - // When killed, sleep returns null exit code - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('--success=last should return last exit code', () => { - // When killed, sleep returns null exit code - return run('node ./src/main.js -k --success last "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') - .then(function(exitCode) { - assert.notStrictEqual(exitCode, 0); - }); - }); - - it('&& nosuchcmd should return non-zero exit code', () => { - return run('node ./src/main.js "echo 1 && nosuchcmd" "echo 1 && nosuchcmd" ') - .then(function(exitCode) { - assert.strictEqual(exitCode, 1); - }); - }); - - it('--prefix-colors should handle non-existent colors without failing', () => { - return run('node ./src/main.js -c "not.a.color" "echo colors"') - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - }); - }); - - it('--prefix should default to "index"', () => { - var collectedLines = [] - - return run('node ./src/main.js "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) - } - } - }) - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - - collectedLines.sort() - assert.deepEqual(collectedLines, [ - '[0] one', - '[1] two' - ]) - }); - }); - - it('--names should set a different default prefix', () => { - var collectedLines = [] - - return run('node ./src/main.js -n aa,bb "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) - } - } - }) - .then(function(exitCode) { - assert.strictEqual(exitCode, 0); - - collectedLines.sort() - assert.deepEqual(collectedLines, [ - '[aa] one', - '[bb] two' - ]) - }); - }); - - it('--allow-restart should restart a proccess with non-zero exit code', (done) => { - var readline = require('readline'); - var exitedWithOne = false; - var restarted = false; - - run('node ./src/main.js --allow-restart "node ./test/support/sleep.js 0.1 && exit 1" "node ./test/support/sleep.js 1"', { - pipe: false, - onOutputLine: (line) => { - var re = /exited with code (.+)/.exec(line); - if (re && re[1] === '1') { - exitedWithOne = true - } - - if (/restarted/.test(line)) { - restarted = true; - } - } - }).then(function() { - if (exitedWithOne && restarted) { - done(); - } else { - done(new Error('No restarted process exited with code 1')); - } - }); - }); - - it('--restart-after=n should restart a proccess after n miliseconds', (done) => { - var readline = require('readline'); - var start, end; - - run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "node ./test/support/sleep.js 1"', { - pipe: false, - onOutputLine: (line) => { - if (!start && /exited with code (.+)/.test(line)) { - start = new Date().getTime(); - } - - if (!end && /restarted/.test(line)) { - end = new Date().getTime(); - } - } - }).then(function() { - // we accept 100 miliseconds of error - if (end - start >= 300 && end - start < 400) { - done(); - } else { - done(new Error('No restarted process after 300 miliseconds - delta is: ' + (end - start))); - } - }); - }); - - it('--restart-tries=n should restart a proccess at most n times', (done) => { - var readline = require('readline'); - var restartedTimes = 0; - - run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "node ./test/support/sleep.js 1"', { - pipe: false, - onOutputLine: (line) => { - if (/restarted/.test(line)) { - restartedTimes++; - } - } - }).then(function() { - if (restartedTimes == 2) { - done(); - } else { - done(new Error('No restarted process twice')); - } - }); - }); - - if (IS_WINDOWS) { - console.log('IS_WINDOWS=true'); - console.log('Skipping SIGINT/SIGTERM propagation tests ...'); - } else { - ['SIGINT', 'SIGTERM'].forEach((signal) => { - it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { - var readline = require('readline'); - var waitingStart = 2; - var waitingSignal = 2; - - function waitForSignal(cb) { - if (waitingSignal) { - setTimeout(waitForSignal, 100); - } else { - cb(); - } - } - - run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { - onOutputLine: function(line, child) { - // waiting for startup - if (/STARTED/.test(line)) { - waitingStart--; - } - if (!waitingStart) { - // both processes are started - child.kill(signal); - } - - // waiting for signal - if (new RegExp(signal).test(line)) { - waitingSignal--; - } - } - }).then(function() { - waitForSignal(done); - }); - }); - }); - } + + it('help should be successful', () => { + return run('node ./src/main.js --help') + .then(function(exitCode) { + // exit code 0 means success + assert.strictEqual(exitCode, 0); + }); + }); + + it('version should be successful', () => { + return run('node ./src/main.js -V') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('two successful commands should exit 0', () => { + return run('node ./src/main.js "echo test" "echo test"') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('at least one unsuccessful commands should exit non-zero', () => { + return run('node ./src/main.js "echo test" "nosuchcmd" "echo test"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others should kill other commands if one dies', () => { + return run('node ./src/main.js --kill-others "node ./test/support/sleep.js 1" "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others-on-fail should kill other commands if one exits with non-zero status code', () => { + return run('node ./src/main.js --kill-others-on-fail "node ./test/support/sleep.js 1" "exit 1" "node ./test/support/sleep.js 1"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('--kill-others-on-fail should NOT kill other commands if none of them exits with non-zero status code', (done) => { + var readline = require('readline'); + var exits = 0; + var sigtermInOutput = false; + + run('node ./src/main.js --kill-others-on-fail "echo killTest1" "echo killTest2" "echo killTest3"', { + onOutputLine: function(line) { + if (/SIGTERM/.test(line)) { + sigtermInOutput = true; + } + + // waiting for exits + if (/killTest\d$/.test(line)) { + exits++; + } + } + }).then(function() { + if (sigtermInOutput) { + done(new Error('There was a "SIGTERM" in console output')); + } else if (exits !== 3) { + done(new Error('There was wrong number of echoes(' + exits + ') from executed commands')); + } else { + done(); + } + }); + }); + + it('--success=first should return first exit code', () => { + return run('node ./src/main.js -k --success first "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + // When killed, sleep returns null exit code + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('--success=last should return last exit code', () => { + // When killed, sleep returns null exit code + return run('node ./src/main.js -k --success last "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') + .then(function(exitCode) { + assert.notStrictEqual(exitCode, 0); + }); + }); + + it('&& nosuchcmd should return non-zero exit code', () => { + return run('node ./src/main.js "echo 1 && nosuchcmd" "echo 1 && nosuchcmd" ') + .then(function(exitCode) { + assert.strictEqual(exitCode, 1); + }); + }); + + it('--prefix-colors should handle non-existent colors without failing', () => { + return run('node ./src/main.js -c "not.a.color" "echo colors"') + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + }); + }); + + it('--prefix should default to "index"', () => { + var collectedLines = [] + + return run('node ./src/main.js "echo one" "echo two"', { + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } + } + }) + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + + collectedLines.sort() + assert.deepEqual(collectedLines, [ + '[0] one', + '[1] two' + ]) + }); + }); + + it('--names should set a different default prefix', () => { + var collectedLines = [] + + return run('node ./src/main.js -n aa,bb "echo one" "echo two"', { + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } + } + }) + .then(function(exitCode) { + assert.strictEqual(exitCode, 0); + + collectedLines.sort() + assert.deepEqual(collectedLines, [ + '[aa] one', + '[bb] two' + ]) + }); + }); + + it('--allow-restart should restart a proccess with non-zero exit code', (done) => { + var readline = require('readline'); + var exitedWithOne = false; + var restarted = false; + + run('node ./src/main.js --allow-restart "node ./test/support/sleep.js 0.1 && exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + var re = /exited with code (.+)/.exec(line); + if (re && re[1] === '1') { + exitedWithOne = true + } + + if (/restarted/.test(line)) { + restarted = true; + } + } + }).then(function() { + if (exitedWithOne && restarted) { + done(); + } else { + done(new Error('No restarted process exited with code 1')); + } + }); + }); + + it('--restart-after=n should restart a proccess after n miliseconds', (done) => { + var readline = require('readline'); + var start, end; + + run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + if (!start && /exited with code (.+)/.test(line)) { + start = new Date().getTime(); + } + + if (!end && /restarted/.test(line)) { + end = new Date().getTime(); + } + } + }).then(function() { + // we accept 100 miliseconds of error + if (end - start >= 300 && end - start < 400) { + done(); + } else { + done(new Error('No restarted process after 300 miliseconds - delta is: ' + (end - start))); + } + }); + }); + + it('--restart-tries=n should restart a proccess at most n times', (done) => { + var readline = require('readline'); + var restartedTimes = 0; + + run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "node ./test/support/sleep.js 1"', { + pipe: false, + onOutputLine: (line) => { + if (/restarted/.test(line)) { + restartedTimes++; + } + } + }).then(function() { + if (restartedTimes == 2) { + done(); + } else { + done(new Error('No restarted process twice')); + } + }); + }); + + if (IS_WINDOWS) { + console.log('IS_WINDOWS=true'); + console.log('Skipping SIGINT/SIGTERM propagation tests ...'); + } else { + ['SIGINT', 'SIGTERM'].forEach((signal) => { + it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { + var readline = require('readline'); + var waitingStart = 2; + var waitingSignal = 2; + + function waitForSignal(cb) { + if (waitingSignal) { + setTimeout(waitForSignal, 100); + } else { + cb(); + } + } + + run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { + onOutputLine: function(line, child) { + // waiting for startup + if (/STARTED/.test(line)) { + waitingStart--; + } + if (!waitingStart) { + // both processes are started + child.kill(signal); + } + + // waiting for signal + if (new RegExp(signal).test(line)) { + waitingSignal--; + } + } + }).then(function() { + waitForSignal(done); + }); + }); + }); + } describe('api', () => { + // TODO: hide output from echo command if DEBUG_TESTS != true it('should run and return correct exit codes', () => { var commands = ['echo 1', 'exit 2']; return concurrently(commands) @@ -282,7 +283,7 @@ describe('concurrently', function() { }); }); it('should use names', () => { - // not sure how to test colors ... + // not sure how to test colors except visually ... var commands = [{ name: 'echo', prefixModifier: 'dim',