diff --git a/History.md b/History.md index f804542..9447bfd 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +### v3.2.1 + - [#31](https://github.com/darkguy2008/parallelshell/issues/31) Report a + missing npm once instead of also marking the script as successful. + Simplify `--npm` and `--timeout` handling and align `--help` output. + ### v3.2.0 - [#31](https://github.com/darkguy2008/parallelshell/issues/31) Add `-n, --npm ` to run npm scripts with wildcard matching, diff --git a/README.md b/README.md index adb019c..a29750c 100644 --- a/README.md +++ b/README.md @@ -64,48 +64,34 @@ argument parser. Available options: ``` --h, --help output usage information --v, --verbose verbose logging --w, --wait will not close sibling processes on error +-h, --help output usage information +-v, --verbose verbose logging +-w, --wait will not close sibling processes on error -t, --timeout stop remaining commands after the deadline --n, --npm run matching npm scripts from package.json - +-n, --npm run matching npm scripts from package.json ``` -Use `-n` (or `--npm`) before each npm script name or pattern: +Use `-n` (or `--npm`) before each npm script name or +[minimatch](https://github.com/isaacs/minimatch) pattern, mixed freely with +ordinary commands: ```bash parallelshell -n "build:*" "echo ordinary command" -n "test:{unit,integration}" ``` -Scripts are read from `package.json` in the current directory and run through -`npm run`, including npm's pre/post lifecycle scripts. npm must be on PATH. -Exact script names take precedence over patterns. Patterns use minimatch syntax: -`*`, `?`, character classes, braces and extended globs such as `build:+(js|css)`. -Matching is case-sensitive, includes names starting with a dot and treats leading -`#` and `!` literally. `/` separates pattern segments; use `**` across segments. -Quote patterns with double quotes so they work on Windows and are not expanded -by your shell. Matches follow package.json order; repeated selections run again. -All selections are validated before any command starts. Missing values, unmatched -patterns and missing or invalid package.json files exit with code 1. - -This shorthand uses the existing shutdown behavior. The npm-script shutdown -limitation on Debian/Ubuntu tracked in #22 remains unchanged. +Scripts come from `package.json` in the current directory and run through +`npm run`, so pre/post scripts still apply. Exact names win over patterns. +Patterns match names starting with `.` and treat a leading `#` or `!` literally. +Every selection is checked before anything starts: a missing value, an unmatched +pattern or an unreadable `package.json` exits with code 1. -Use `--timeout` (or `-t`) to limit the overall run, including when `--wait` is -enabled: +Use `-t` (or `--timeout`) to stop everything still running after a number of +seconds, including with `--wait`: ```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. +On timeout parallelshell exits with code 124, or with an earlier failure's code +under `--wait`. Commands are stopped the same way as on failure, so on Unix a +command that ignores SIGINT can outlive the deadline. diff --git a/index.js b/index.js index f18d5ab..d238649 100755 --- a/index.js +++ b/index.js @@ -2,9 +2,7 @@ 'use strict'; var spawn = require('child_process').spawn; -var spawnNpm = require('cross-spawn'); var fs = require('fs'); -var minimatch = require('minimatch'); var path = require('path'); var signals = require('constants'); var SIGNAL_EXIT_CODE_BASE = 128; @@ -13,6 +11,7 @@ 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 SCRIPT_PATTERN_OPTIONS = { dot: true, nocomment: true, nonegate: true, allowWindowsEscape: true }; var WINDOWS = process.platform === 'win32'; var commandPrefix = WINDOWS ? '' : 'exec '; @@ -24,22 +23,9 @@ for (i = 0, len = args.length; i < len; i++) { switch (args[i]) { case '-n': case '--npm': - var pattern = args[++i]; - if (!pattern || pattern[0] === '-') { - console.error('--npm requires a script name or pattern'); - process.exit(1); - } try { - if (!scripts) { - scripts = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).scripts; - if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw new Error('package.json must contain a scripts object'); - } - var names = Object.keys(scripts); - var matches = Object.prototype.hasOwnProperty.call(scripts, pattern) ? [pattern] : minimatch.match(names, pattern, { dot: true, nocomment: true, nonegate: true, allowWindowsEscape: true }); - if (!matches.length) throw new Error('no npm scripts match ' + JSON.stringify(pattern)); - matches.forEach(function (name) { - if (typeof scripts[name] !== 'string') throw new Error('npm script ' + JSON.stringify(name) + ' must be a string'); - cmds.push({ script: name }); + matchingScripts(args[++i]).forEach(function (name) { + cmds.push({ cmd: 'npm run -- ' + name, script: name }); }); } catch (error) { console.error('--npm: ' + error.message); @@ -48,8 +34,8 @@ for (i = 0, len = args.length; i < len; i++) { break; case '-t': case '--timeout': - timeout = Number(args[++i]) * MILLISECONDS_PER_SECOND; - if (!isFinite(timeout) || timeout <= 0 || timeout > MAX_TIMEOUT_MS) { + timeout = Number(args[++i]); + if (!(timeout > 0 && timeout * MILLISECONDS_PER_SECOND <= MAX_TIMEOUT_MS)) { console.error('--timeout requires positive seconds no greater than ' + MAX_TIMEOUT_MS / MILLISECONDS_PER_SECOND); process.exit(1); } @@ -64,23 +50,36 @@ for (i = 0, len = args.length; i < len; i++) { 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('-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 stop remaining commands after the deadline'); - console.log('-n, --npm run matching npm scripts from package.json'); + console.log('-n, --npm run matching npm scripts from package.json'); process.exit(); break; } } else { - cmds.push(args[i]); + cmds.push({ cmd: commandPrefix + args[i] }); } } +function matchingScripts (pattern) { + if (!pattern || pattern[0] === '-') throw new Error('requires a script name or pattern'); + scripts = scripts || JSON.parse(fs.readFileSync('package.json', 'utf8')).scripts; + if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw new Error('package.json must contain a scripts object'); + var matches = Object.prototype.hasOwnProperty.call(scripts, pattern) ? [pattern] : require('minimatch').match(Object.keys(scripts), pattern, SCRIPT_PATTERN_OPTIONS); + if (!matches.length) throw new Error('no npm scripts match ' + JSON.stringify(pattern)); + matches.forEach(function (name) { + if (typeof scripts[name] !== 'string') throw new Error('npm script ' + JSON.stringify(name) + ' must be a string'); + }); + return matches; +} + +function isRunning (child) { + return child.exitCode === null && child.signalCode === null; +} + 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) { @@ -104,7 +103,7 @@ function status () { console.log('\n'); console.log('### Status ###'); for (i = 0, len = children.length; i < len; i++) { - if (children[i].exitCode === null && children[i].signalCode === null) { + if (isRunning(children[i])) { console.log('`' + children[i].cmd + '` is still running'); } else if (children[i].exitCode !== 0) { console.log('`' + children[i].cmd + '` errored'); @@ -118,9 +117,7 @@ function status () { function close (signal) { clearTimeout(timer); - var running = children.filter(function (child) { - return child.exitCode === null && child.signalCode === null; - }); + var running = children.filter(isRunning); var remaining = running.length; if (remaining === 0) return exit(signal); running.forEach(function (child) { @@ -162,29 +159,24 @@ FORWARDED_SIGNALS.forEach(function (signal) { }); children = cmds.map(function (cmd) { - var command = cmd.script === undefined ? commandPrefix + cmd : 'npm run -- ' + JSON.stringify(cmd.script); - var options = { - stdio: ['pipe', process.stdout, process.stderr] - }; - var child; - if (cmd.script === undefined) { - options.shell = true; - child = spawn(command, options); - } else { - child = spawnNpm('npm', ['run', '--', cmd.script], options); - } + var stdio = ['pipe', process.stdout, process.stderr]; + var child = cmd.script === undefined + ? spawn(cmd.cmd, { shell: true, stdio: stdio }) + : require('cross-spawn')('npm', ['run', '--', cmd.script], { stdio: stdio }); child.on('error', function (error) { console.error(error.message); + this.removeListener('close', childClose); childClose.call(this, 1); }).on('close', childClose); - child.cmd = command; + child.cmd = cmd.cmd; return child; }); -if (timeout && children.length) { +if (timeout) { timer = setTimeout(function () { - console.error('parallelshell timed out after ' + timeout / MILLISECONDS_PER_SECOND + ' seconds'); + console.error('parallelshell timed out after ' + timeout + ' seconds'); process.exitCode = process.exitCode || TIMEOUT_EXIT_CODE; close(); - }, timeout); + }, timeout * MILLISECONDS_PER_SECOND); + timer.unref(); } diff --git a/package.json b/package.json index c1138ed..cc7c111 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parallelshell", - "version": "3.2.0", + "version": "3.2.1", "description": "Invoke multiple commands, running in parallel", "homepage": "https://github.com/darkguy2008/parallelshell", "repository": { diff --git a/test/index.coffee b/test/index.coffee index 222bb5b..541dd24 100644 --- a/test/index.coffee +++ b/test/index.coffee @@ -27,9 +27,11 @@ POWERSHELL_ARGS = ["-NoProfile", "-ExecutionPolicy", "Bypass"] ENV_NAME = "PARALLELSHELL_TEST_ENV" ENV_VALUE = "passed-through" QUOTED_TEXT = "two spaces" -TIMEOUT_SECONDS = 2 +TIMEOUT_SECONDS = "2" TIMEOUT_EXIT_CODE = 124 -COMPLETION_TIMEOUT_SECONDS = 60 +COMPLETION_TIMEOUT_SECONDS = "60" +FRACTIONAL_TIMEOUT_SECONDS = "0.001" +PACKAGE_FILES = ["package.json", "npm-debug.log"] fixture = (name, args...) -> [process.execPath, path.join(FIXTURES_DIR, name)].concat(args).join " " exitProcess = (code) -> fixture "exit.js", code @@ -39,21 +41,23 @@ printCwdProcess = "#{process.execPath} -p \"process.cwd()\"" printEnvProcess = "#{process.execPath} -p process.env.#{ENV_NAME}" usageInfo = """ --h, --help output usage information --v, --verbose verbose logging --w, --wait will not close sibling processes on error +-h, --help output usage information +-v, --verbose verbose logging +-w, --wait will not close sibling processes on error -t, --timeout stop remaining commands after the deadline --n, --npm run matching npm scripts from package.json +-n, --npm run matching npm scripts from package.json """ + "\n" spawned = [] -triggerDirectories = [] +directories = [] -newTrigger = -> - directory = path.join os.tmpdir(), "parallelshell-#{process.pid}-#{Date.now()}-#{triggerDirectories.length}" +newDirectory = -> + directory = path.join os.tmpdir(), "parallelshell-#{process.pid}-#{Date.now()}-#{directories.length}" fs.mkdirSync directory - triggerDirectories.push directory - path.join directory, TRIGGER_NAME + directories.push directory + directory + +newTrigger = -> path.join newDirectory(), TRIGGER_NAME release = (trigger) -> fs.writeFileSync trigger, "" @@ -107,27 +111,31 @@ isAlive = (pid) -> doneCount = (ps) -> outputLines(ps).filter((line) -> line == DONE_LINE).length +shouldRejectBeforeLaunch = (ps, message) -> + ps.exited.then (result) -> + result.code.should.equal 1 + ps.output.should.equal "" + ps.errorOutput.should.contain message + afterEach -> for ps in spawned.splice(0) if WINDOWS childProcess.spawnSync "taskkill", ["/T", "/F", "/PID", String ps.pid] if ps.exitCode == null and ps.signalCode == null else if isAlive -ps.pid process.kill -ps.pid, "SIGKILL" - for directory in triggerDirectories.splice(0) - trigger = path.join directory, TRIGGER_NAME - fs.unlinkSync trigger if fs.existsSync trigger - manifest = path.join directory, "package.json" - fs.unlinkSync manifest if fs.existsSync manifest - npmLog = path.join directory, "npm-debug.log" - fs.unlinkSync npmLog if fs.existsSync npmLog + for directory in directories.splice(0) + for name in [TRIGGER_NAME].concat PACKAGE_FILES + file = path.join directory, name + fs.unlinkSync file if fs.existsSync file fs.rmdirSync directory describe "parallelshell", -> describe "npm scripts", -> - scriptNames = ["build:js", "build:css", "build:html", "test:js", ".hidden", "#hash", "!bang", "literal*", "two words", "quote\"name", "amp&name", "percent%PATH%", "nested/build/js"] + literalNames = ["literal*", "#hash", "!bang", "two words", "quote\"name", "amp&name", "percent%PATH%"] + scriptNames = ["build:js", "build:css", "build:html", "test:js", ".hidden", "nested/build/js"].concat literalNames project = (scripts) -> - directory = path.dirname newTrigger() + directory = newDirectory() fs.writeFileSync path.join(directory, "package.json"), JSON.stringify {name: "parallelshell-fixture", version: "1.0.0", scripts} directory @@ -139,6 +147,8 @@ describe "parallelshell", -> runScripts = (args...) -> spawnParallelshellWith {cwd: npmProject()}, args... + scriptLines = (ps) -> outputLines(ps).filter((line) -> line.indexOf("script ") == 0).sort() + ["-n", "--npm"].forEach (flag) -> it "should run an exact npm script with #{flag}", -> ps = runScripts flag, "build:js" @@ -153,14 +163,9 @@ describe "parallelshell", -> "build:{js,css}": ["build:js", "build:css"] "build:[ch]*": ["build:css", "build:html"] "build:+(js|css)": ["build:js", "build:css"] - "literal*": ["literal*"] ".*": [".hidden"] - "#hash": ["#hash"] - "!bang": ["!bang"] - "two words": ["two words"] - "quote\"name": ["quote\"name"] - "amp&name": ["amp&name"] - "percent%PATH%": ["percent%PATH%"] + "#h*": ["#hash"] + "!b*": ["!bang"] "nested/**": ["nested/build/js"] } Object.keys(patterns).forEach (pattern) -> @@ -168,10 +173,18 @@ describe "parallelshell", -> ps = runScripts "-n", pattern ps.exited.then (result) -> result.code.should.equal 0, ps.output + ps.errorOutput - outputLines(ps).filter((line) -> line.indexOf("script ") == 0).sort().should.deep.equal patterns[pattern].map((name) -> "script #{name}").sort() + scriptLines(ps).should.deep.equal patterns[pattern].map((name) -> "script #{name}").sort() + + it "should run script names containing pattern and shell characters literally", -> + args = [] + args.push "-n", name for name in literalNames + ps = runScripts args... + ps.exited.then (result) -> + result.code.should.equal 0, ps.output + ps.errorOutput + scriptLines(ps).should.deep.equal literalNames.map((name) -> "script #{name}").sort() it "should mix repeated npm options with ordinary commands and existing options", -> - ps = runScripts "-w", "-n", "build:js", "echo ordinary", "--npm", "build:css", "-t", String(COMPLETION_TIMEOUT_SECONDS) + ps = runScripts "-w", "-n", "build:js", "echo ordinary", "--npm", "build:css", "-t", COMPLETION_TIMEOUT_SECONDS ps.exited.then (result) -> result.code.should.equal 0 outputLines(ps).should.include.members ["ordinary", "script build:js", "script build:css"] @@ -192,51 +205,47 @@ describe "parallelshell", -> ps = spawnParallelshellWith {cwd: project({fail: exitProcess(FAILURE_EXIT_CODE)})}, "-n", "fail" ps.exited.then (result) -> result.code.should.not.equal 0 + it "should fail once when npm is not on PATH", -> + env = {} + env[name] = value for name, value of process.env when name.toUpperCase() != "PATH" + env.PATH = newDirectory() + ps = spawnParallelshellWith {cwd: npmProject(), env}, "-w", "-v", "-n", "build:js" + ps.exited.then (result) -> + result.code.should.equal 1 + hasLineEndingWith(SUCCESS_SUFFIX)(ps).should.be.false + [undefined, "", "-w", "missing", "missing:*"].forEach (pattern) -> it "should reject #{pattern} before launching any command", -> args = ["echo ordinary", "-n", "build:js", "-n"] args.push pattern if pattern != undefined - ps = runScripts args... - ps.exited.then (result) -> - result.code.should.equal 1 - ps.output.should.equal "" - ps.errorOutput.should.contain "--npm" + shouldRejectBeforeLaunch runScripts(args...), "--npm" [null, [], {bad: 42}].forEach (scripts) -> it "should reject missing or invalid script definitions #{JSON.stringify scripts}", -> - ps = spawnParallelshellWith {cwd: project(scripts)}, "echo ordinary", "-n", "bad" - ps.exited.then (result) -> - result.code.should.equal 1 - ps.output.should.equal "" - ps.errorOutput.should.contain "--npm" + shouldRejectBeforeLaunch spawnParallelshellWith({cwd: project(scripts)}, "echo ordinary", "-n", "bad"), "--npm" it "should reject invalid package JSON before launching commands", -> directory = project {} fs.writeFileSync path.join(directory, "package.json"), "{" - ps = spawnParallelshellWith {cwd: directory}, "echo ordinary", "-n", "build" - ps.exited.then (result) -> - result.code.should.equal 1 - ps.output.should.equal "" - ps.errorOutput.should.contain "--npm" + shouldRejectBeforeLaunch spawnParallelshellWith({cwd: directory}, "echo ordinary", "-n", "build"), "--npm" it "should reject a missing package.json before launching commands", -> - ps = spawnParallelshellWith {cwd: path.dirname(newTrigger())}, "echo ordinary", "-n", "build" + shouldRejectBeforeLaunch spawnParallelshellWith({cwd: newDirectory()}, "echo ordinary", "-n", "build"), "--npm" + + it "should stop running commands at the deadline", -> + ps = spawnParallelshell "--timeout", TIMEOUT_SECONDS, succeedingProcess, waitingProcess(), waitingProcess() + waitForReady(ps, 2).then (pids) -> ps.exited.then (result) -> - result.code.should.equal 1 - ps.output.should.equal "" - ps.errorOutput.should.contain "--npm" + 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 ["-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 accept fractional timeout seconds with #{flag}", -> + spawnParallelshell(flag, FRACTIONAL_TIMEOUT_SECONDS, waitingProcess()).exited.then (result) -> result.code.should.equal TIMEOUT_EXIT_CODE it "should time out with --wait and preserve an earlier failure", -> - ps = spawnParallelshell "--wait", "-v", "--timeout", String(TIMEOUT_SECONDS), exitProcess(FAILURE_EXIT_CODE), waitingProcess() + ps = spawnParallelshell "--wait", "-v", "--timeout", TIMEOUT_SECONDS, exitProcess(FAILURE_EXIT_CODE), waitingProcess() Promise.all [waitForReady(ps, 1), waitForOutput(ps, hasLineEndingWith ERRORED_SUFFIX)] .then ([pids]) -> ps.exited.then (result) -> @@ -245,42 +254,28 @@ describe "parallelshell", -> 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 = spawnParallelshell "--wait", "--timeout", FRACTIONAL_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 + completions = { + "every command finishes": [[succeedingProcess, succeedingProcess], 0] + "a command fails": [[exitProcess FAILURE_EXIT_CODE], FAILURE_EXIT_CODE] + "all commands finish with --wait after a failure": [["--wait", exitProcess(FAILURE_EXIT_CODE), succeedingProcess], FAILURE_EXIT_CODE] + "there are no commands": [[], 0] + } + Object.keys(completions).forEach (description) -> + [args, code] = completions[description] + it "should exit without timing out when #{description}", -> + ps = spawnParallelshell "--timeout", COMPLETION_TIMEOUT_SECONDS, args... + ps.exited.then (result) -> + result.code.should.equal code + ps.errorOutput.should.equal "" [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" + shouldRejectBeforeLaunch spawnParallelshell(args...), "--timeout requires positive seconds" it "should print on -h and --help", -> Promise.all ["-h", "--help"].map (flag) -> @@ -371,7 +366,7 @@ describe "parallelshell", -> FORWARDED_SIGNALS.forEach (signal) -> onPosix "should stop its children with #{signal} and die by #{signal} without crashing", -> - ps = spawnParallelshell "--timeout", String(COMPLETION_TIMEOUT_SECONDS), waitingProcess(), waitingProcess() + ps = spawnParallelshell "--timeout", COMPLETION_TIMEOUT_SECONDS, waitingProcess(), waitingProcess() waitForReady(ps, 2).then (pids) -> ps.kill signal ps.exited.then (result) ->