diff --git a/README.md b/README.md index 718dac5..bcc7b3b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,13 @@ This will execute the commands `echo 1` `echo 2` and `echo 3` simultaneously. Note that on Windows, you need to use double-quotes to avoid confusing the argument parser. +##### Nested usage + +```bash +parallelshell "echo 1" "parallelshell 'echo 2' 'parallelshell \'echo 3\' \'parallelshell \'\'echo 4\'\' \''" +``` +Closing on Windows will be unreliable in this case.. + Available options: ``` -h, --help output usage information diff --git a/index.js b/index.js index c93f8a0..f3fd1aa 100755 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ 'use strict'; var spawn = require('child_process').spawn; -var sh, shFlag, children, args, wait, cmds, verbose, i ,len; +var sh, shFlag, children, args, wait, first, cmds, verbose, i ,len; // parsing argv cmds = []; args = process.argv.slice(2); @@ -14,6 +14,10 @@ for (i = 0, len = args.length; i < len; i++) { case '--wait': wait = true; break; + case '-f': + case '--first': + first = true; + break; case '-v': case '--verbose': verbose = true; @@ -21,8 +25,9 @@ for (i = 0, len = args.length; i < len; i++) { case '-h': case '--help': console.log('-h, --help output usage information'); - console.log('-v, --verbose verbose logging') - console.log('-w, --wait will not close sibling processes on error') + console.log('-v, --verbose verbose logging'); + console.log('-w, --wait will not close sibling processes on error'); + console.log('-f, --first close all sibling processes after first exits (succes/error)'); process.exit(); break; } @@ -31,55 +36,65 @@ for (i = 0, len = args.length; i < len; i++) { } } +if (wait && first) { + console.error('--wait and --first cannot be used together'); + process.exit(1); +} + // called on close of a child process function childClose (code) { var i, len; code = code ? (code.code || code) : code; if (verbose) { if (code > 0) { - console.error('`' + this.cmd + '` failed with exit code ' + code); + console.error('parallelshell: `' + this.cmd + '` failed with exit code ' + code); } else { - console.log('`' + this.cmd + '` ended successfully'); + console.log('parallelshell: `' + this.cmd + '` ended successfully'); } } - if (code > 0 && !wait) close(code); - status(); + if (first || code > 0 && !wait) close(code); } function status () { if (verbose) { var i, len; - console.log('\n'); - console.log('### Status ###'); + console.log('parallelshell: Status'); for (i = 0, len = children.length; i < len; i++) { if (children[i].exitCode === null) { - console.log('`' + children[i].cmd + '` is still running'); + console.log('parallelshell: `' + children[i].cmd + '` is still running'); } else if (children[i].exitCode > 0) { - console.log('`' + children[i].cmd + '` errored'); + console.log('parallelshell: `' + children[i].cmd + '` errored'); } else { - console.log('`' + children[i].cmd + '` finished'); + console.log('parallelshell: `' + children[i].cmd + '` finished'); } } - console.log('\n'); } } // closes all children and the process function close (code) { - var i, len, closed = 0, opened = 0; + var i, len, closeHandler, closed = 0, opened = 0; for (i = 0, len = children.length; i < len; i++) { - if (!children[i].exitCode) { + if (children[i].exitCode === null) { opened++; children[i].removeAllListeners('close'); - children[i].kill("SIGINT"); - if (verbose) console.log('`' + children[i].cmd + '` will now be closed'); - children[i].on('close', function() { - closed++; - if (opened == closed) { - process.exit(code); - } - }); + if (process.platform != "win32") { + spawn(sh, [shFlag, "kill -INT -"+children[i].pid]); + } else { + children[i].kill("SIGINT"); + } + if (verbose) console.log('parallelshell: `' + children[i].cmd + '` will now be closed'); + closeHandler = function (child) { + child.on('close', function() { + if (verbose) console.log('parallelshell: `' + child.cmd + '` closed successfully'); + closed++; + if (opened == closed) { + process.exit(code); + } + }); + }(children[i]) + } } if (opened == closed) {process.exit(code);} @@ -98,13 +113,15 @@ if (process.platform === 'win32') { // start the children children = []; cmds.forEach(function (cmd) { - if (process.platform != 'win32') { - cmd = "exec "+cmd; + if (process.platform === 'win32') { + cmd = cmd.replace(/'/g,"\""); } var child = spawn(sh,[shFlag,cmd], { cwd: process.cwd, env: process.env, - stdio: ['pipe', process.stdout, process.stderr] + stdio: ['pipe', process.stdout, process.stderr], + windowsVerbatimArguments: process.platform === 'win32', + detached: process.platform != 'win32' }) .on('close', childClose); child.cmd = cmd @@ -112,4 +129,11 @@ cmds.forEach(function (cmd) { }); // close all children on ctrl+c -process.on('SIGINT', close) +process.on('SIGINT', function() { + if (verbose) console.log('parallelshell: recieved SIGINT'); + close(); +}); + +process.on('exit', function(code) { + if (verbose) console.log('parallelshell: exit code:', code); +}); diff --git a/test/index.coffee b/test/index.coffee index 02aebba..0b433a9 100644 --- a/test/index.coffee +++ b/test/index.coffee @@ -14,7 +14,10 @@ else shArg = "-c" # children -waitingProcess = "\"node -e 'setTimeout(function(){},10000);'\"" +waitingProcess = (time=10000) -> + return "\"node -e 'setTimeout(function(){},#{time});'\"" +waitingFailingProcess = (time=10000) -> + return "\"node -e 'setTimeout(function(){ throw new Error(); },#{time});'\"" failingProcess = "\"node -e 'throw new Error();'\"" usageInfo = """ @@ -24,8 +27,6 @@ usageInfo = """ """.split("\n") cmdWrapper = (cmd) -> - if process.platform != "win32" - cmd = "exec "+cmd if verbose console.log "Calling: "+cmd return cmd @@ -33,10 +34,18 @@ cmdWrapper = (cmd) -> spawnParallelshell = (cmd) -> return spawn sh, [shArg, cmdWrapper("node ./index.js "+cmd )], { cwd: process.cwd + windowsVerbatimArguments: process.platform == 'win32' + detached: process.platform != 'win32' } killPs = (ps) -> - ps.kill "SIGINT" + if verbose + console.log "killing" + if process.platform == "win32" + killer = spawn sh, [shArg, "taskkill /F /T /PID "+ps.pid] + else + killer = spawn sh, [shArg, "kill -INT -"+ps.pid] + spyOnPs killer, 3 spyOnPs = (ps, verbosity=1) -> if verbose >= verbosity @@ -47,17 +56,21 @@ spyOnPs = (ps, verbosity=1) -> ps.stderr.on "data", (data) -> console.log "err: "+data -testOutput = (cmd, expectedOutput) -> +testOutput = (cmd, expectedOutput, std="out") -> return new Promise (resolve) -> ps = spawnParallelshell(cmd) + if std == "out" + std = ps.stdout + else + std = ps.stderr spyOnPs ps, 3 - ps.stdout.setEncoding("utf8") + std.setEncoding("utf8") output = [] - ps.stdout.on "data", (data) -> + std.on "data", (data) -> lines = data.split("\n") lines.pop() if lines[lines.length-1] == "" output = output.concat(lines) - ps.stdout.on "end", () -> + std.on "end", () -> for line,i in expectedOutput line.should.equal output[i] resolve() @@ -75,29 +88,33 @@ describe "parallelshell", -> ps.exitCode.should.equal 1 done() + it "should close with exitCode 1 on delayed child error", (done) -> + ps = spawnParallelshell([waitingFailingProcess(100),waitingProcess(1),waitingProcess(500)].join(" ")) + spyOnPs ps, 2 + ps.on "exit", () -> + ps.exitCode.should.equal 1 + done() + it "should run with a normal child", (done) -> - ps = spawnParallelshell(waitingProcess) + ps = spawnParallelshell(waitingProcess()) spyOnPs ps, 1 ps.on "close", () -> - ps.signalCode.should.equal "SIGINT" done() - setTimeout (() -> should.not.exist(ps.signalCode) killPs(ps) - ),50 - + ),150 it "should close sibling processes on child error", (done) -> - ps = spawnParallelshell([waitingProcess,failingProcess,waitingProcess].join(" ")) + ps = spawnParallelshell([waitingProcess(),failingProcess].join(" ")) spyOnPs ps,2 - ps.on "close", () -> + ps.on "exit", () -> ps.exitCode.should.equal 1 done() it "should wait for sibling processes on child error when called with -w or --wait", (done) -> - ps = spawnParallelshell(["-w",waitingProcess,failingProcess,waitingProcess].join(" ")) - ps2 = spawnParallelshell(["--wait",waitingProcess,failingProcess,waitingProcess].join(" ")) + ps = spawnParallelshell(["-w",waitingProcess(),failingProcess].join(" ")) + ps2 = spawnParallelshell(["--wait",waitingProcess(),failingProcess].join(" ")) spyOnPs ps,2 spyOnPs ps2,2 setTimeout (() -> @@ -105,15 +122,51 @@ describe "parallelshell", -> should.not.exist(ps2.signalCode) killPs(ps) killPs(ps2) - ),50 + ),250 Promise.all [new Promise((resolve) -> ps.on("close",resolve)), new Promise (resolve) -> ps2.on("close",resolve)] .then -> done() .catch done it "should close on CTRL+C / SIGINT", (done) -> - ps = spawnParallelshell(["-w",waitingProcess,failingProcess,waitingProcess].join(" ")) + ps = spawnParallelshell(["-w",waitingProcess()].join(" ")) spyOnPs ps,2 ps.on "close", () -> - ps.signalCode.should.equal "SIGINT" done() killPs(ps) + it "should work with chained commands", (done) -> + output = ["1","2"] + if process.platform == "win32" + output[0] += "\r" + output[1] += "\r" + testOutput("\"echo 1&& echo 2\"", output) + .then done + .catch done + it "should work nested", (done) -> + output = ["1","2"] + if process.platform == "win32" + output[0] += "\r" + output[1] += "\r" + testOutput("\"echo 1\" \"node ./index.js 'echo 2'\"", output) + .then done + .catch done + + it "should work with setting ENV", (done) -> + output = ["test1"] + if process.platform == "win32" + setString = "set test=test1&" + else + setString = "test=test1 " + testOutput("\"#{setString}node -e 'console.log(process.env.test);'\"", output) + .then done + .catch done + + it "should work with first", (done) -> + ps = spawnParallelshell(["--first",waitingProcess(10),waitingProcess(10000)].join(" ")) + ps.on "exit", () -> + ps.exitCode.should.equal 0 + done() + + it "should not work with first and wait", (done) -> + testOutput("--wait --first", ["--wait and --first cannot be used together"], "err") + .then done + .catch done