Skip to content
Merged
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
5 changes: 5 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v3.1.0
- [#40](https://github.com/darkguy2008/parallelshell/issues/40) Add
`-t, --timeout <seconds>` to stop remaining commands after a deadline,
including with `--wait`.

### v3.0.4
- [#41](https://github.com/darkguy2008/parallelshell/issues/41) Mention
`& wait` in the README.
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,24 @@ Available options:
-h, --help output usage information
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-t, --timeout <seconds> stop remaining commands after the deadline

```

Use `--timeout` (or `-t`) to limit the overall run, including when `--wait` is
enabled:

```bash
parallelshell --timeout 10 "node server.js" "node request.js"
```

The deadline starts after the commands are launched. Seconds may be fractional
and must be positive and no greater than 2147483.647. If commands are still
running at the deadline, parallelshell reports the timeout and exits with code
124, or preserves an earlier non-zero command exit code with `--wait`. Commands
that finish before the deadline retain their normal exit status.

Timeouts use the existing shutdown behavior: SIGINT to direct children on Unix,
and forced process-tree termination on Windows. On Unix this is not a guaranteed
hard deadline: commands that ignore SIGINT or leave descendants running may keep
the run alive.
26 changes: 25 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ var signals = require('constants');
var SIGNAL_EXIT_CODE_BASE = 128;
var FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
var WINDOWS_CONTROL_C_EXIT = 0xC000013A;
var TIMEOUT_EXIT_CODE = 124;
var MILLISECONDS_PER_SECOND = 1000;
var MAX_TIMEOUT_MS = Math.pow(2, 31) - 1;

var WINDOWS = process.platform === 'win32';
var commandPrefix = WINDOWS ? '' : 'exec ';
var children, args, wait, cmds, verbose, i ,len;
var children, args, wait, cmds, verbose, timeout, timer, i ,len;
cmds = [];
args = process.argv.slice(2);
for (i = 0, len = args.length; i < len; i++) {
if (args[i][0] === '-') {
switch (args[i]) {
case '-t':
case '--timeout':
timeout = Number(args[++i]) * MILLISECONDS_PER_SECOND;
if (!isFinite(timeout) || timeout <= 0 || timeout > MAX_TIMEOUT_MS) {
console.error('--timeout requires positive seconds no greater than ' + MAX_TIMEOUT_MS / MILLISECONDS_PER_SECOND);
process.exit(1);
}
break;
case '-w':
case '--wait':
wait = true;
Expand All @@ -29,6 +40,7 @@ for (i = 0, len = args.length; i < len; i++) {
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('-t, --timeout <seconds> stop remaining commands after the deadline');
process.exit();
break;
}
Expand All @@ -38,6 +50,9 @@ for (i = 0, len = args.length; i < len; i++) {
}

function childClose (code, signal) {
if (children.every(function (child) {
return child.exitCode !== null || child.signalCode !== null;
})) clearTimeout(timer);
code = signal ? SIGNAL_EXIT_CODE_BASE + signals[signal] : code;
if (verbose) {
if (code > 0) {
Expand Down Expand Up @@ -74,6 +89,7 @@ function status () {
}

function close (signal) {
clearTimeout(timer);
var running = children.filter(function (child) {
return child.exitCode === null && child.signalCode === null;
});
Expand Down Expand Up @@ -126,3 +142,11 @@ children = cmds.map(function (cmd) {
child.cmd = command;
return child;
});

if (timeout && children.length) {
timer = setTimeout(function () {
console.error('parallelshell timed out after ' + timeout / MILLISECONDS_PER_SECOND + ' seconds');
process.exitCode = process.exitCode || TIMEOUT_EXIT_CODE;
close();
}, timeout);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parallelshell",
"version": "3.0.4",
"version": "3.1.0",
"description": "Invoke multiple commands, running in parallel",
"homepage": "https://github.com/darkguy2008/parallelshell",
"repository": {
Expand Down
62 changes: 61 additions & 1 deletion test/index.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ POWERSHELL_ARGS = ["-NoProfile", "-ExecutionPolicy", "Bypass"]
ENV_NAME = "PARALLELSHELL_TEST_ENV"
ENV_VALUE = "passed-through"
QUOTED_TEXT = "two spaces"
TIMEOUT_SECONDS = 2
TIMEOUT_EXIT_CODE = 124
COMPLETION_TIMEOUT_SECONDS = 60

fixture = (name, args...) -> [process.execPath, path.join(FIXTURES_DIR, name)].concat(args).join " "
exitProcess = (code) -> fixture "exit.js", code
Expand All @@ -39,6 +42,7 @@ usageInfo = """
-h, --help output usage information
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-t, --timeout <seconds> stop remaining commands after the deadline
""" + "\n"

spawned = []
Expand Down Expand Up @@ -114,6 +118,62 @@ afterEach ->
fs.rmdirSync directory

describe "parallelshell", ->
["-t", "--timeout"].forEach (flag) ->
it "should stop running commands at the deadline with #{flag}", ->
ps = spawnParallelshell flag, String(TIMEOUT_SECONDS), succeedingProcess, waitingProcess(), waitingProcess()
waitForReady(ps, 2).then (pids) ->
ps.exited.then (result) ->
result.should.deep.equal {code: TIMEOUT_EXIT_CODE, signal: null}
ps.errorOutput.should.contain "timed out after #{TIMEOUT_SECONDS} seconds"
pids.filter(isAlive).should.be.empty

it "should time out with --wait and preserve an earlier failure", ->
ps = spawnParallelshell "--wait", "-v", "--timeout", String(TIMEOUT_SECONDS), exitProcess(FAILURE_EXIT_CODE), waitingProcess()
Promise.all [waitForReady(ps, 1), waitForOutput(ps, hasLineEndingWith ERRORED_SUFFIX)]
.then ([pids]) ->
ps.exited.then (result) ->
result.code.should.equal FAILURE_EXIT_CODE
ps.errorOutput.should.contain "timed out"
pids.filter(isAlive).should.be.empty

it "should time out successful commands that remain running with --wait", ->
ps = spawnParallelshell "--wait", "--timeout", String(TIMEOUT_SECONDS), waitingProcess()
ps.exited.then (result) -> result.code.should.equal TIMEOUT_EXIT_CODE

it "should cancel a timeout when every command finishes", ->
ps = spawnParallelshell "--timeout", String(COMPLETION_TIMEOUT_SECONDS), succeedingProcess, succeedingProcess
ps.exited.then (result) ->
result.code.should.equal 0
ps.errorOutput.should.equal ""

it "should cancel a timeout when a command fails", ->
ps = spawnParallelshell "--timeout", String(COMPLETION_TIMEOUT_SECONDS), exitProcess FAILURE_EXIT_CODE
ps.exited.then (result) ->
result.code.should.equal FAILURE_EXIT_CODE
ps.errorOutput.should.not.contain "timed out"

it "should retain the failure code when all commands finish with --wait", ->
ps = spawnParallelshell "--wait", "--timeout", String(COMPLETION_TIMEOUT_SECONDS), exitProcess(FAILURE_EXIT_CODE), succeedingProcess
ps.exited.then (result) ->
result.code.should.equal FAILURE_EXIT_CODE
ps.errorOutput.should.not.contain "timed out"

it "should exit successfully with a timeout and no commands", ->
spawnParallelshell("--timeout", String(COMPLETION_TIMEOUT_SECONDS)).exited.then (result) -> result.code.should.equal 0

it "should accept fractional timeout seconds", ->
spawnParallelshell("--timeout", "0.001", waitingProcess()).exited.then (result) -> result.code.should.equal TIMEOUT_EXIT_CODE

[undefined, "0", "-1", "nope", "Infinity", "2147483.648", "--wait"].forEach (value) ->
it "should reject invalid timeout #{value} before launching commands", ->
args = [succeedingProcess, "--timeout"]
args.push value if value != undefined
ps = spawnParallelshell args...
ps.exited.then (result) ->
result.code.should.equal 1
ps.output.should.equal ""
ps.errorOutput.should.contain "--timeout requires positive seconds"

it "should print on -h and --help", ->
Promise.all ["-h", "--help"].map (flag) ->
ps = spawnParallelshell flag
Expand Down Expand Up @@ -203,7 +263,7 @@ describe "parallelshell", ->

FORWARDED_SIGNALS.forEach (signal) ->
onPosix "should stop its children with #{signal} and die by #{signal} without crashing", ->
ps = spawnParallelshell waitingProcess(), waitingProcess()
ps = spawnParallelshell "--timeout", String(COMPLETION_TIMEOUT_SECONDS), waitingProcess(), waitingProcess()
waitForReady(ps, 2).then (pids) ->
ps.kill signal
ps.exited.then (result) ->
Expand Down
Loading