-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
161 lines (141 loc) · 4.69 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
const chalk = require("chalk");
[("uncaughtException", "unhandledRejection")].forEach((badThing) => {
process.on(badThing, (err) => {
console.error(chalk.red(badThing, err, err.stack));
});
});
const timerHoldingProcessOpen = setInterval(() => {
// do nothing
}, 1 << 30);
const fs = require("fs");
const Spinnies = require("spinnies");
const { spawn } = require("child_process");
const path = require("path");
const logsByProcess = {};
const failures = [];
function logFailures() {
spinnies.stopAll();
for (const failure of failures) {
console.log(`\n——————————${failure}——————————\n`);
console.log((logsByProcess[failure] || []).join("\n"));
}
}
const spinnies = new Spinnies();
const HELP_TEXT = `## Usage
require("allelify")([
"make test",
{
title: "lint", // overrides lint
command: "make lint",
},
{
title: "snarglify",
command: "ag",
args: ["-l", "build steps"], // args is necessary when arguments have spaces
},
], { tmpDirectory: path.join(__dirname, "../tmp") });
If you've installed this globally using \`npm install -g allelify\`, you can run things in parallel on the command line: \`allelify 'sleep 1' 'sleep 2'\`
`;
// there's probably a better way of doing this—just want a reasonable date string for a file name
function getDateTimestampForFilename(d = new Date()) {
return new Date().toISOString().replace(/[-:\/.]/g, "_");
}
module.exports = function runCommandsInParallel(commands, config = {}) {
return new Promise((resolve, reject) => {
let hasPromiseFinalized = false;
function logErrorAndRejrect(msg, err) {
spinnies.stopAll();
console.error(
err ?? new Error(msg),
`\n${chalk.red(msg)}`,
`\n\n${HELP_TEXT}\n`
);
if (!hasPromiseFinalized) {
reject(err || new Error(msg));
hasPromiseFinalized = true;
}
}
const tmpDirectory = config.tmpDirectory ?? "/tmp";
try {
fs.statSync(tmpDirectory);
} catch (err) {
return logErrorAndRejrect(`Unable to stat ${tmpDirectory}`, err);
}
const sharedTimestamp = getDateTimestampForFilename();
let running = 0;
for (const commandObj of commands) {
let command;
let title;
let args;
if (typeof commandObj === "string") {
title = commandObj;
[command, ...args] = (commandObj ?? "").split(" ");
} else if (commandObj.args) {
title = commandObj.title;
command = commandObj.command;
args = commandObj.args;
} else {
title = commandObj.title;
[command, ...args] = (commandObj.command ?? "").split(" ");
}
if (!title || !command) {
return logErrorAndRejrect("Missing required title or command");
}
const p = path.join(
tmpDirectory,
`${sharedTimestamp}-${title}-command.log`.replace(/[ \s:/]/g, "_")
);
try {
fs.writeFileSync(p, "", "utf-8"); // clear out any existing file
} catch (err) {
return logErrorAndRejrect(
`Unable to write to ${p}. Try setting a different temporary directory for it to write command output to`,
err
);
}
const commandString = `${command} ${args}`;
spinnies.add(title, { text: `${title}` });
const writeStream = new fs.createWriteStream(p, "utf-8");
running++;
const subProcess = spawn(command, args, {
stderr: writeStream,
stdout: writeStream,
})
.on("error", (err) => {
spinnies.fail(title, {
text: `Unable to start ${title} — ${commandString}`,
});
console.error(err);
throw err;
})
.on("close", (code) => {
if (!code) {
spinnies.succeed(title, { text: `${title}` });
} else {
failures.push(title);
spinnies.fail(title, {
text: `${title} failed. You can find the logs in ${p.toString()} and they will be logged out after all processes complete. To re-run the command run ${commandString}`,
});
process.exitCode = code;
}
running--;
if (running === 0) {
timerHoldingProcessOpen.unref();
if (process.exitCode) {
reject(new Error("Some commands errored"));
} else {
resolve();
}
logFailures();
}
});
const addLog = (data) => {
fs.appendFileSync(p, String(data) + "\n", "utf-8");
logsByProcess[title] ??= [];
logsByProcess[title].push(data);
};
subProcess.stderr.on("data", addLog);
subProcess.stdout.on("data", addLog);
}
});
};