nx
Version:
418 lines (416 loc) • 16.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ForkedProcessTaskRunner = void 0;
const fs_1 = require("fs");
const child_process_1 = require("child_process");
const chalk = require("chalk");
const output_1 = require("../utils/output");
const utils_1 = require("./utils");
const path_1 = require("path");
const batch_messages_1 = require("./batch/batch-messages");
const strip_indents_1 = require("../utils/strip-indents");
const stream_1 = require("stream");
const pseudo_terminal_1 = require("./pseudo-terminal");
const exit_codes_1 = require("../utils/exit-codes");
const forkScript = (0, path_1.join)(__dirname, './fork.js');
const workerPath = (0, path_1.join)(__dirname, './batch/run-batch.js');
class ForkedProcessTaskRunner {
constructor(options) {
this.options = options;
this.cliPath = (0, utils_1.getCliPath)();
this.verbose = process.env.NX_VERBOSE_LOGGING === 'true';
this.processes = new Set();
this.pseudoTerminal = pseudo_terminal_1.PseudoTerminal.isSupported()
? (0, pseudo_terminal_1.getPseudoTerminal)()
: null;
}
async init() {
if (this.pseudoTerminal) {
await this.pseudoTerminal.init();
}
this.setupProcessEventListeners();
}
// TODO: vsavkin delegate terminal output printing
forkProcessForBatch({ executorName, taskGraph: batchTaskGraph }, projectGraph, fullTaskGraph, env) {
return new Promise((res, rej) => {
try {
const count = Object.keys(batchTaskGraph.tasks).length;
if (count > 1) {
output_1.output.logSingleLine(`Running ${output_1.output.bold(count)} ${output_1.output.bold('tasks')} with ${output_1.output.bold(executorName)}`);
}
else {
const args = (0, utils_1.getPrintableCommandArgsForTask)(Object.values(batchTaskGraph.tasks)[0]);
output_1.output.logCommand(args.join(' '));
}
const p = (0, child_process_1.fork)(workerPath, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env,
});
this.processes.add(p);
p.once('exit', (code, signal) => {
this.processes.delete(p);
if (code === null)
code = (0, exit_codes_1.signalToCode)(signal);
if (code !== 0) {
const results = {};
for (const rootTaskId of batchTaskGraph.roots) {
results[rootTaskId] = {
success: false,
terminalOutput: '',
};
}
rej(new Error(`"${executorName}" exited unexpectedly with code: ${code}`));
}
});
p.on('message', (message) => {
switch (message.type) {
case batch_messages_1.BatchMessageType.CompleteBatchExecution: {
res(message.results);
break;
}
case batch_messages_1.BatchMessageType.RunTasks: {
break;
}
default: {
// Re-emit any non-batch messages from the task process
if (process.send) {
process.send(message);
}
}
}
});
// Start the tasks
p.send({
type: batch_messages_1.BatchMessageType.RunTasks,
executorName,
projectGraph,
batchTaskGraph,
fullTaskGraph,
});
}
catch (e) {
rej(e);
}
});
}
async forkProcessLegacy(task, { temporaryOutputPath, streamOutput, pipeOutput, taskGraph, env, }) {
return pipeOutput
? await this.forkProcessPipeOutputCapture(task, {
temporaryOutputPath,
streamOutput,
taskGraph,
env,
})
: await this.forkProcessDirectOutputCapture(task, {
temporaryOutputPath,
streamOutput,
taskGraph,
env,
});
}
async forkProcess(task, { temporaryOutputPath, streamOutput, taskGraph, env, disablePseudoTerminal, }) {
const shouldPrefix = streamOutput && process.env.NX_PREFIX_OUTPUT === 'true';
// streamOutput would be false if we are running multiple targets
// there's no point in running the commands in a pty if we are not streaming the output
if (!this.pseudoTerminal ||
disablePseudoTerminal ||
!streamOutput ||
shouldPrefix) {
return this.forkProcessWithPrefixAndNotTTY(task, {
temporaryOutputPath,
streamOutput,
taskGraph,
env,
});
}
else {
return this.forkProcessWithPseudoTerminal(task, {
temporaryOutputPath,
streamOutput,
taskGraph,
env,
});
}
}
async forkProcessWithPseudoTerminal(task, { temporaryOutputPath, streamOutput, taskGraph, env, }) {
const args = (0, utils_1.getPrintableCommandArgsForTask)(task);
if (streamOutput) {
output_1.output.logCommand(args.join(' '));
}
const childId = task.id;
const p = await this.pseudoTerminal.fork(childId, forkScript, {
cwd: process.cwd(),
execArgv: process.execArgv,
jsEnv: env,
quiet: !streamOutput,
});
p.send({
targetDescription: task.target,
overrides: task.overrides,
taskGraph,
isVerbose: this.verbose,
});
this.processes.add(p);
let terminalOutput = '';
p.onOutput((msg) => {
terminalOutput += msg;
});
return new Promise((res) => {
p.onExit((code) => {
// If the exit code is greater than 128, it's a special exit code for a signal
if (code >= 128) {
process.exit(code);
}
this.writeTerminalOutput(temporaryOutputPath, terminalOutput);
res({
code,
terminalOutput,
});
});
});
}
forkProcessPipeOutputCapture(task, { streamOutput, temporaryOutputPath, taskGraph, env, }) {
return this.forkProcessWithPrefixAndNotTTY(task, {
streamOutput,
temporaryOutputPath,
taskGraph,
env,
});
}
forkProcessWithPrefixAndNotTTY(task, { streamOutput, temporaryOutputPath, taskGraph, env, }) {
return new Promise((res, rej) => {
try {
const args = (0, utils_1.getPrintableCommandArgsForTask)(task);
if (streamOutput) {
output_1.output.logCommand(args.join(' '));
}
const p = (0, child_process_1.fork)(this.cliPath, {
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
env,
});
this.processes.add(p);
// Re-emit any messages from the task process
p.on('message', (message) => {
if (process.send) {
process.send(message);
}
});
// Send message to run the executor
p.send({
targetDescription: task.target,
overrides: task.overrides,
taskGraph,
isVerbose: this.verbose,
});
if (streamOutput) {
if (process.env.NX_PREFIX_OUTPUT === 'true') {
const color = getColor(task.target.project);
const prefixText = `${task.target.project}:`;
p.stdout
.pipe(logClearLineToPrefixTransformer(color.bold(prefixText) + ' '))
.pipe(addPrefixTransformer(color.bold(prefixText)))
.pipe(process.stdout);
p.stderr
.pipe(logClearLineToPrefixTransformer(color(prefixText) + ' '))
.pipe(addPrefixTransformer(color(prefixText)))
.pipe(process.stderr);
}
else {
p.stdout.pipe(addPrefixTransformer()).pipe(process.stdout);
p.stderr.pipe(addPrefixTransformer()).pipe(process.stderr);
}
}
let outWithErr = [];
p.stdout.on('data', (chunk) => {
outWithErr.push(chunk.toString());
});
p.stderr.on('data', (chunk) => {
outWithErr.push(chunk.toString());
});
p.on('exit', (code, signal) => {
this.processes.delete(p);
if (code === null)
code = (0, exit_codes_1.signalToCode)(signal);
// we didn't print any output as we were running the command
// print all the collected output|
const terminalOutput = outWithErr.join('');
if (!streamOutput) {
this.options.lifeCycle.printTaskTerminalOutput(task, code === 0 ? 'success' : 'failure', terminalOutput);
}
this.writeTerminalOutput(temporaryOutputPath, terminalOutput);
res({ code, terminalOutput });
});
}
catch (e) {
console.error(e);
rej(e);
}
});
}
forkProcessDirectOutputCapture(task, { streamOutput, temporaryOutputPath, taskGraph, env, }) {
return new Promise((res, rej) => {
try {
const args = (0, utils_1.getPrintableCommandArgsForTask)(task);
if (streamOutput) {
output_1.output.logCommand(args.join(' '));
}
const p = (0, child_process_1.fork)(this.cliPath, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env,
});
this.processes.add(p);
// Re-emit any messages from the task process
p.on('message', (message) => {
if (process.send) {
process.send(message);
}
});
// Send message to run the executor
p.send({
targetDescription: task.target,
overrides: task.overrides,
taskGraph,
isVerbose: this.verbose,
});
p.on('exit', (code, signal) => {
if (code === null)
code = (0, exit_codes_1.signalToCode)(signal);
// we didn't print any output as we were running the command
// print all the collected output
let terminalOutput = '';
try {
terminalOutput = this.readTerminalOutput(temporaryOutputPath);
if (!streamOutput) {
this.options.lifeCycle.printTaskTerminalOutput(task, code === 0 ? 'success' : 'failure', terminalOutput);
}
}
catch (e) {
console.log((0, strip_indents_1.stripIndents) `
Unable to print terminal output for Task "${task.id}".
Task failed with Exit Code ${code} and Signal "${signal}".
Received error message:
${e.message}
`);
}
res({
code,
terminalOutput,
});
});
}
catch (e) {
console.error(e);
rej(e);
}
});
}
readTerminalOutput(outputPath) {
return (0, fs_1.readFileSync)(outputPath).toString();
}
writeTerminalOutput(outputPath, content) {
(0, fs_1.writeFileSync)(outputPath, content);
}
setupProcessEventListeners() {
if (this.pseudoTerminal) {
this.pseudoTerminal.onMessageFromChildren((message) => {
process.send(message);
});
}
// When the nx process gets a message, it will be sent into the task's process
process.on('message', (message) => {
// this.publisher.publish(message.toString());
if (this.pseudoTerminal) {
this.pseudoTerminal.sendMessageToChildren(message);
}
this.processes.forEach((p) => {
if ('connected' in p && p.connected) {
p.send(message);
}
});
});
// Terminate any task processes on exit
process.on('exit', () => {
this.processes.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill();
}
});
});
process.on('SIGINT', () => {
this.processes.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// we exit here because we don't need to write anything to cache.
process.exit((0, exit_codes_1.signalToCode)('SIGINT'));
});
process.on('SIGTERM', () => {
this.processes.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// no exit here because we expect child processes to terminate which
// will store results to the cache and will terminate this process
});
process.on('SIGHUP', () => {
this.processes.forEach((p) => {
if ('connected' in p ? p.connected : p.isAlive) {
p.kill('SIGTERM');
}
});
// no exit here because we expect child processes to terminate which
// will store results to the cache and will terminate this process
});
}
}
exports.ForkedProcessTaskRunner = ForkedProcessTaskRunner;
const colors = [
chalk.green,
chalk.greenBright,
chalk.red,
chalk.redBright,
chalk.cyan,
chalk.cyanBright,
chalk.yellow,
chalk.yellowBright,
chalk.magenta,
chalk.magentaBright,
];
function getColor(projectName) {
let code = 0;
for (let i = 0; i < projectName.length; ++i) {
code += projectName.charCodeAt(i);
}
const colorIndex = code % colors.length;
return colors[colorIndex];
}
/**
* Prevents terminal escape sequence from clearing line prefix.
*/
function logClearLineToPrefixTransformer(prefix) {
let prevChunk = null;
return new stream_1.Transform({
transform(chunk, _encoding, callback) {
if (prevChunk && prevChunk.toString() === '\x1b[2K') {
chunk = chunk.toString().replace(/\x1b\[1G/g, (m) => m + prefix);
}
this.push(chunk);
prevChunk = chunk;
callback();
},
});
}
function addPrefixTransformer(prefix) {
const newLineSeparator = process.platform.startsWith('win') ? '\r\n' : '\n';
return new stream_1.Transform({
transform(chunk, _encoding, callback) {
const list = chunk.toString().split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g);
list
.filter(Boolean)
.forEach((m) => this.push(prefix ? prefix + ' ' + m + newLineSeparator : m + newLineSeparator));
callback();
},
});
}
;