Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #30

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open

Dev #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 51 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -14,15 +14,20 @@ 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;
break;
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)');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

succes is spelled success.

process.exit();
break;
}
Expand All @@ -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);}
Expand All @@ -98,18 +113,27 @@ 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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any output when I run commands now. Seems this line is the culprit. Why are we detaching the process?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So they can be killed properly and we don't end up with zombie processes. I actually think L122 is the culprit more than L124

})
.on('close', childClose);
child.cmd = cmd
children.push(child)
});

// 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);
});
93 changes: 73 additions & 20 deletions test/index.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand All @@ -24,19 +27,25 @@ usageInfo = """
""".split("\n")

cmdWrapper = (cmd) ->
if process.platform != "win32"
cmd = "exec "+cmd
if verbose
console.log "Calling: "+cmd
return 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
Expand All @@ -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()
Expand All @@ -75,45 +88,85 @@ 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 (() ->
should.not.exist(ps.signalCode)
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