UNPKG

too

Version:

Combine multiple processes' Stdout/Stderr/SIGINT to keep them all foreground

113 lines (112 loc) 4.29 kB
import { getColorByIndex } from './colors.js'; import { Command } from './command.js'; import { DefaultLogger } from './logger.js'; export class SequentialExecutor { state; definition; env; logger; steps = []; constructor(state, definition = { steps: [] }, env = {}, logger = new DefaultLogger()) { this.state = state; this.definition = definition; this.env = env; this.logger = logger; for (let i = 0; i < definition.steps.length; i++) { const step = definition.steps[i]; const cmd = new Command(step.run, env, getColorByIndex(i), logger, step.label); this.steps.push(cmd); } const maxWidth = this.steps.reduce((max, cmd) => Math.max(max, cmd.label.length), 0); this.logger.maxLabelWidth = Math.max(this.logger.maxLabelWidth, maxWidth); } async run() { if (this.steps.length === 0) return; this.logger.stage(this.state); for (let i = 0; i < this.steps.length; i++) { const step = this.steps[i]; step.env = { ...step.env, ...this.env }; try { await step.exec(); } catch (e) { // Honor `ignore_error`: log the failure and continue instead of aborting // the stage (e.g. `post: pkill datastore`, which exits non-zero when no // process matches). Any failure is swallowed, including command-not-found. (#531) if (!this.definition.steps[i]?.ignore_error) throw e; const err = e; const reason = err.message || err.msg || `exit code ${err.code}`; this.logger.output(step.label, step.color, "", `ignored error: ${reason}`); } } } } export class ParallelExecutor { state; definition; env; logger; jobs = []; procs = []; constructor(state, definition, env = {}, logger = new DefaultLogger()) { this.state = state; this.definition = definition; this.env = env; this.logger = logger; for (let i = 0; i < definition.jobs.length; i++) { const job = definition.jobs[i]; const cmd = new Command(job.run, env, getColorByIndex(i), logger, job.label); this.jobs.push(cmd); } const maxWidth = this.jobs.reduce((max, cmd) => Math.max(max, cmd.label.length), 0); this.logger.maxLabelWidth = Math.max(this.logger.maxLabelWidth, maxWidth); } async run() { this.logger.stage(this.state); for (const job of this.jobs) { job.env = { ...job.env, ...this.env }; this.procs.push(await job.start()); } await this.wait(); } async cleanup(signal) { const promises = this.procs.map((proc) => { return new Promise((resolve) => { // Already exited (e.g. the job that triggered the failure): nothing to kill. if (proc.exitCode !== null || proc.signalCode !== null) return resolve(true); // Resolve only once the child has actually closed, so callers can await teardown. proc.on("close", () => resolve(true)); try { // Negative pid signals the whole process group (requires detached children). if (proc.pid) process.kill(-proc.pid, signal); else resolve(true); } catch (err) { const error = err; if (error.code === "ESRCH") return resolve(true); console.error(error.message); resolve(false); } }); }); return Promise.all(promises); } wait() { return Promise.all(this.procs.map((proc) => { return new Promise((resolve, reject) => { proc.on("exit", (code, signal) => { if (code !== 0) reject({ code, signal }); else resolve(code); }); }); })); } }