UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

285 lines (283 loc) • 11.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ForkedProcessTaskRunner = void 0; const fs_1 = require("fs"); const child_process_1 = require("child_process"); 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 pseudo_terminal_1 = require("./pseudo-terminal"); const exit_codes_1 = require("../utils/exit-codes"); const node_child_process_1 = require("./running-tasks/node-child-process"); const batch_process_1 = require("./running-tasks/batch-process"); const native_1 = require("../native"); const forkScript = (0, path_1.join)(__dirname, './fork.js'); const workerPath = (0, path_1.join)(__dirname, './batch/run-batch.js'); class ForkedProcessTaskRunner { constructor(options, tuiEnabled) { this.options = options; this.tuiEnabled = tuiEnabled; this.cliPath = (0, utils_1.getCliPath)(); this.verbose = process.env.NX_VERBOSE_LOGGING === 'true'; this.processes = new Set(); this.finishedProcesses = new Set(); this.pseudoTerminals = new Set(); } async init() { this.setupProcessEventListeners(); } // TODO: vsavkin delegate terminal output printing async forkProcessForBatch({ executorName, taskGraph: batchTaskGraph }, projectGraph, fullTaskGraph, env) { 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, }); const cp = new batch_process_1.BatchProcess(p, executorName); this.processes.add(cp); cp.onExit(() => { this.processes.delete(cp); }); // Start the tasks cp.send({ type: batch_messages_1.BatchMessageType.RunTasks, executorName, projectGraph, batchTaskGraph, fullTaskGraph, }); return cp; } cleanUpBatchProcesses() { if (this.finishedProcesses.size > 0) { this.finishedProcesses.forEach((p) => { p.kill(); }); this.finishedProcesses.clear(); } } async forkProcessLegacy(task, { temporaryOutputPath, streamOutput, pipeOutput, taskGraph, env, }) { return pipeOutput ? this.forkProcessWithPrefixAndNotTTY(task, { temporaryOutputPath, streamOutput, taskGraph, env, }) : this.forkProcessDirectOutputCapture(task, { temporaryOutputPath, streamOutput, taskGraph, env, }); } async forkProcess(task, { temporaryOutputPath, streamOutput, taskGraph, env, disablePseudoTerminal, }) { const shouldPrefix = streamOutput && process.env.NX_PREFIX_OUTPUT === 'true' && !this.tuiEnabled; // 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 (pseudo_terminal_1.PseudoTerminal.isSupported() && !disablePseudoTerminal && (this.tuiEnabled || (streamOutput && !shouldPrefix))) { // Use pseudo-terminal for interactive tasks that can support user input return this.forkProcessWithPseudoTerminal(task, { temporaryOutputPath, streamOutput, taskGraph, env, }); } else { // Use non-interactive process with piped output // Tradeoff: These tasks cannot support interactivity but can still provide // progressive output to the TUI if it's enabled return this.forkProcessWithPrefixAndNotTTY(task, { temporaryOutputPath, streamOutput, taskGraph, env, }); } } async createPseudoTerminal() { const terminal = new pseudo_terminal_1.PseudoTerminal(new native_1.RustPseudoTerminal()); await terminal.init(); terminal.onMessageFromChildren((message) => { process.send(message); }); return terminal; } async forkProcessWithPseudoTerminal(task, { temporaryOutputPath, streamOutput, taskGraph, env, }) { const childId = task.id; const pseudoTerminal = await this.createPseudoTerminal(); this.pseudoTerminals.add(pseudoTerminal); const p = await pseudoTerminal.fork(childId, forkScript, { cwd: process.cwd(), execArgv: process.execArgv, jsEnv: env, quiet: !streamOutput, commandLabel: `nx run ${task.id}`, }); p.send({ targetDescription: task.target, overrides: task.overrides, taskGraph, isVerbose: this.verbose, }); this.processes.add(p); let terminalOutput = ''; p.onOutput((msg) => { terminalOutput += msg; }); p.onExit((code) => { if (!this.tuiEnabled && code > 128) { process.exit(code); } this.pseudoTerminals.delete(pseudoTerminal); this.processes.delete(p); if (!streamOutput) { this.options.lifeCycle.printTaskTerminalOutput(task, code === 0 ? 'success' : 'failure', terminalOutput); } this.writeTerminalOutput(temporaryOutputPath, terminalOutput); }); return p; } forkProcessWithPrefixAndNotTTY(task, { streamOutput, temporaryOutputPath, taskGraph, env, }) { 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, }); // Send message to run the executor p.send({ targetDescription: task.target, overrides: task.overrides, taskGraph, isVerbose: this.verbose, }); const cp = new node_child_process_1.NodeChildProcessWithNonDirectOutput(p, { streamOutput, prefix: task.target.project, }); this.processes.add(cp); cp.onExit((code, terminalOutput) => { this.processes.delete(cp); if (!streamOutput) { this.options.lifeCycle.printTaskTerminalOutput(task, code === 0 ? 'success' : 'failure', terminalOutput); } this.writeTerminalOutput(temporaryOutputPath, terminalOutput); }); return cp; } catch (e) { console.error(e); throw e; } } forkProcessDirectOutputCapture(task, { streamOutput, temporaryOutputPath, taskGraph, env, }) { 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, }); const cp = new node_child_process_1.NodeChildProcessWithDirectOutput(p, temporaryOutputPath); this.processes.add(cp); // Send message to run the executor p.send({ targetDescription: task.target, overrides: task.overrides, taskGraph, isVerbose: this.verbose, }); cp.onExit((code, signal) => { this.processes.delete(cp); // we didn't print any output as we were running the command // print all the collected output try { const terminalOutput = cp.getTerminalOutput(); 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} `); } }); return cp; } catch (e) { console.error(e); throw e; } } writeTerminalOutput(outputPath, content) { (0, fs_1.writeFileSync)(outputPath, content); } cleanup(signal) { this.processes.forEach((p) => { p.kill(signal); }); this.cleanUpBatchProcesses(); } setupProcessEventListeners() { const messageHandler = (message) => { this.pseudoTerminals.forEach((p) => { p.sendMessageToChildren(message); }); this.processes.forEach((p) => { if ('connected' in p && p.connected && 'send' in p) { p.send(message); } }); }; // When the nx process gets a message, it will be sent into the task's process process.on('message', messageHandler); // Terminate any task processes on exit process.once('exit', () => { this.cleanup(); process.off('message', messageHandler); }); process.once('SIGINT', () => { this.cleanup('SIGTERM'); process.off('message', messageHandler); // we exit here because we don't need to write anything to cache. process.exit((0, exit_codes_1.signalToCode)('SIGINT')); }); process.once('SIGTERM', () => { this.cleanup('SIGTERM'); process.off('message', messageHandler); // no exit here because we expect child processes to terminate which // will store results to the cache and will terminate this process }); process.once('SIGHUP', () => { this.cleanup('SIGTERM'); process.off('message', messageHandler); // 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;