Skip to content

Commit

Permalink
Kill all tasks when one of tasks exited with errors.
Browse files Browse the repository at this point in the history
refs #6

Because npm does not forward signals to child processes, I lookup child
processes of npm, and kill them.
  • Loading branch information
mysticatea committed Jun 19, 2015
1 parent cf60bdf commit 87e7864
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "mysticatea/nodejs",
"rules": {
"spaced-comment": [2, "always", {"exceptions": ["*", "-"]}]
"spaced-comment": 0
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"test-task:append:a:c": "node test/tasks/append.js ac",
"test-task:append:a:d": "node test/tasks/append.js ad",
"test-task:append:b": "node test/tasks/append.js b",
"test-task:append2": "node test/tasks/append2.js",
"test-task:error": "node test/tasks/error.js",
"test-task:stdio": "node test/tasks/stdio.js"
},
"dependencies": {
"es6-promise": "^2.3.0",
"minimatch": "^2.0.8"
"minimatch": "^2.0.8",
"which": "^1.1.1"
},
"devDependencies": {
"babel": "^5.5.8",
Expand Down
52 changes: 37 additions & 15 deletions src/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,27 +82,49 @@ export default function main(
return Promise.reject(err);
}

return (function next() {
const group = queue.shift();
if (group == null) {
return SUCCESS;
}
if (group.tasks.length === 0) {
return next();
let currentPromise = null;
let aborted = false;
const resultPromise = queue.reduce((prevPromise, group) => {
return prevPromise.then(() => {
if (group == null || group.tasks.length === 0 || aborted) {
return undefined;
}

currentPromise = runAll(
group.tasks,
{stdout, stderr, parallel: group.parallel});

return currentPromise;
});
}, SUCCESS);

// Define abort method.
resultPromise.abort = function abort() {
aborted = true;
if (currentPromise != null) {
currentPromise.abort();
}
};

const options = {
stdout,
stderr,
parallel: group.parallel
};
return runAll(group.tasks, options).then(next);
})();
return resultPromise;
}

/* eslint no-process-exit:0 */
/* istanbul ignore if */
if (require.main === module) {
main(process.argv.slice(2)).catch(err => {
// Execute.
const promise = main(process.argv.slice(2));

// SIGINT/SIGTERM Handling.
process.on("SIGINT", () => {
promise.abort();
});
process.on("SIGTERM", () => {
promise.abort();
});

// Error Handling.
promise.catch(err => {
console.error("ERROR:", err.message); // eslint-disable-line no-console
process.exit(1);
});
Expand Down
113 changes: 70 additions & 43 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {spawn} from "child_process";
import {join} from "path";
import minimatch from "minimatch";
import Promise from "./promise";
import runTask from "./run-task";

//------------------------------------------------------------------------------
function toArray(x) {
if (x == null) {
return [];
Expand Down Expand Up @@ -72,53 +73,83 @@ function filterTasks(taskList, patterns) {
}

//------------------------------------------------------------------------------
function defineExec() {
if (process.platform === "win32") {
const FILE = process.env.comspec || "cmd.exe";
const OPTIONS = {windowsVerbatimArguments: true};
return command => spawn(FILE, ["/s", "/c", `"${command}"`], OPTIONS);
}
return command => spawn("/bin/sh", ["-c", command]);
}
function runAllSequencially(tasks, stdin, stdout, stderr) {
let currentPromise = null;
let aborted = false;
const resultPromise = tasks.reduce((prevPromise, task) => {
return prevPromise.then(() => {
if (aborted) {
return undefined;
}

const exec = defineExec();
currentPromise = runTask(task, stdin, stdout, stderr);
return currentPromise.then(item => {
currentPromise = null;
if (item.code !== 0) {
throw new Error(
`${item.task}: None-Zero Exit(${item.code});`);
}
});
});
}, Promise.resolve());

function runTask(task, stdin, stdout, stderr) {
return new Promise((resolve, reject) => {
// Execute.
const cp = exec(`npm run-script ${task}`);
// Define abort method.
resultPromise.abort = function abort() {
aborted = true;
if (currentPromise != null) {
currentPromise.kill();
}
};

// Piping stdio.
if (stdin) { stdin.pipe(cp.stdin); }
if (stdout) { cp.stdout.pipe(stdout); }
if (stderr) { cp.stderr.pipe(stderr); }
return resultPromise;
}

// Register
cp.on("exit", code => {
if (code) {
reject(new Error(`${task}: None-Zero Exit(${code});`));
}
else {
resolve(null);
}
});
cp.on("error", reject);
//------------------------------------------------------------------------------
function runAllInParallel(tasks, stdin, stdout, stderr) {
// When one of tasks exited with non-zero, kill all tasks.
// And wait for all tasks exit.
let nonZeroExited = null;
const taskPromises = tasks.map(task => runTask(task, stdin, stdout, stderr));
const parallelPromise = Promise.all(taskPromises.map(p => p.then(item => {
if (item.code !== 0) {
nonZeroExited = nonZeroExited || item;
taskPromises.forEach(t => { t.kill(); });
}
})));
parallelPromise.catch(() => {
taskPromises.forEach(t => { t.kill(); });
});

// Make fail if there are tasks that exited non-zero.
const resultPromise = parallelPromise.then(() => {
if (nonZeroExited != null) {
throw new Error(
`${nonZeroExited.task}: None-Zero Exit(${nonZeroExited.code});`);
}
});

// Define abort method.
resultPromise.abort = function abort() {
taskPromises.forEach(t => { t.kill(); });
};

return resultPromise;
}

//------------------------------------------------------------------------------
export default function runAll(_tasks, _options) {
const patterns = toArray(_tasks);
export default function runAll(patternOrPatterns, options = {}) {
const patterns = toArray(patternOrPatterns);
if (patterns.length === 0) {
return Promise.resolve(null);
}

const options = _options || {};
const parallel = Boolean(options.parallel);
const stdin = options.stdin || null;
const stdout = options.stdout || null;
const stderr = options.stderr || null;
const taskList = options.taskList || readTaskList();
const {
parallel = false,
stdin = null,
stdout = null,
stderr = null,
taskList = readTaskList()
} = options;

if (Array.isArray(taskList) === false) {
return Promise.reject(new Error(
Expand All @@ -132,11 +163,7 @@ export default function runAll(_tasks, _options) {
`Matched tasks not found: ${patterns.join(", ")}`));
}

if (parallel) {
return Promise.all(tasks.map(task => runTask(task, stdin, stdout, stderr)));
}
return (function next() {
const task = tasks.shift();
return task && runTask(task, stdin, stdout, stderr).then(next);
})();
return parallel
? runAllInParallel(tasks, stdin, stdout, stderr)
: runAllSequencially(tasks, stdin, stdout, stderr);
}
157 changes: 157 additions & 0 deletions src/run-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {spawn} from "child_process";
import which from "which";
import Promise from "./promise";

//------------------------------------------------------------------------------
function lookupNpm() {
const cwd = process.cwd();
if (lookupNpm.cache[cwd] == null) {
lookupNpm.cache[cwd] = new Promise((resolve, reject) => {
which("npm", (err, npmPath) => {
if (err != null) {
reject(err);
}
else {
resolve(npmPath);
}
});
});
}
return lookupNpm.cache[cwd];
}
lookupNpm.cache = Object.create(null);

//------------------------------------------------------------------------------
function isWrapped(s) {
return (
(s[0] === "\"" && s[s.length - 1] === "\"") ||
(s[0] === "'" && s[s.length - 1] === "'")
);
}

function makeNpmArgs(task) { // eslint-disable-line complexity
const retv = ["run-script"];

let start = 0;
let inSq = false;
let inDq = false;
for (let i = 0; i < task.length; ++i) {
switch (task[i]) {
case " ":
if (!inSq && !inDq) {
const s = task.slice(start, i).trim();
if (s.length > 0) {
retv.push(s);
}
start = i;
}
break;

case "'":
if (!inDq) {
inSq = !inSq;
}
break;

case "\"":
if (!inSq) {
inDq = !inDq;
}
break;

default:
break;
}
}

const s = task.slice(start).trim();
if (s.length > 0) {
retv.push(isWrapped(s) ? s.slice(1, -1) : s);
}

return retv;
}

//------------------------------------------------------------------------------
const killTask = (function defineKillTask() {
if (process.platform === "win32") {
return function killTask(cp) { // eslint-disable-line no-shadow
spawn("taskkill", ["/F", "/T", "/PID", cp.pid]);
};
}

function lookupChildren(pid, cb) {
const cp = spawn("ps", ["--no-headers", "--format", "pid", "--ppid", String(pid)]);
let children = "";

cp.stdout.setEncoding("utf8");
cp.stdout.on("data", chunk => {
children += chunk;
});

cp.on("error", cb);
cp.on("close", () => {
const list = children.split(/\s+/).filter(x => x);
cb(list);
});
}
return function killTask(cp) { // eslint-disable-line no-shadow
// npm does not forward signals to child processes.
// We must kill those.
lookupChildren(cp.pid, shPids => {
const shPid = shPids[0];
if (!shPid) {
cp.kill();
}
else {
lookupChildren(shPid, children => {
const args = ["-s", "15"].concat(children, [shPid, cp.pid]);
spawn("kill", args);
});
}
});
};
})();

//------------------------------------------------------------------------------
/**
* @param {string} task - A task name to run.
* @param {stream.Readable|null} stdin - A readable stream for stdin of child process.
* @param {stream.Writable|null} stdout - A writable stream for stdout of child process.
* @param {stream.Writable|null} stderr - A writable stream for stderr of child process.
* @returns {Promise}
* A promise that becomes fulfilled when task finished.
* This promise object has a extra method: `kill()`.
*/
export default function runTask(task, stdin, stdout, stderr) {
let cp = null;
const promise = lookupNpm().then(npmPath => {
return new Promise((resolve, reject) => {
// Execute.
cp = spawn(npmPath, makeNpmArgs(task));

// Piping stdio.
if (stdin) { stdin.pipe(cp.stdin); }
if (stdout) { cp.stdout.pipe(stdout); }
if (stderr) { cp.stderr.pipe(stderr); }

// Register
cp.on("error", err => {
cp = null;
reject(err);
});
cp.on("close", code => {
cp = null;
resolve({task, code});
});
});
});

promise.kill = function kill() {
if (cp != null) {
killTask(cp);
}
};

return promise;
}
1 change: 0 additions & 1 deletion test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,4 @@ describe("npm-run-all", () => {
});
});
});

});

0 comments on commit 87e7864

Please sign in to comment.