import chalk from "chalk"; import { ChildProcess } from "child_process"; import spawn from "cross-spawn"; import { Cancelable, debounce } from "lodash"; import psTree from "pstree.remy"; import { Executor } from "./types"; interface LongRunningExecutorOptions { args?: ReadonlyArray; /** Specify the period in which the process is restarted at max once. */ debounce?: number; } export default class LongRunningExecutor implements Executor { private cmd: string; private args?: ReadonlyArray; private process: ChildProcess | null = null; private isRunning = false; private shouldRestart = false; private restartDebounced: (() => void) & Cancelable; constructor(cmd: string, opts: LongRunningExecutorOptions = {}) { this.cmd = cmd; this.args = opts.args; this.restartDebounced = debounce( () => this.restart(), opts.debounce || 500 ); } private spawnProcess() { this.isRunning = true; this.process = spawn(this.cmd, this.args as string[], { stdio: "inherit", shell: !this.args, }); this.process.on("exit", (code: number) => { this.isRunning = false; if (code !== 0 && code !== null) { // eslint-disable-next-line no-console console.log(chalk.red(`Command exited with ${code}`)); return; } if (this.shouldRestart) { this.shouldRestart = false; this.spawnProcess(); } }); } private restart() { this.shouldRestart = true; return this.internalKill(); } private kill() { this.shouldRestart = false; return this.internalKill(); } private async internalKill(): Promise { return new Promise((resolve, reject) => { const signal = "SIGTERM"; if (process.platform === "win32") { // Force kill (/F) the whole child tree (/T) by PID spawn.sync("taskkill", [ "/pid", this.process!.pid.toString(), "/T", "/F", ]); resolve(); return; } psTree(this.process!.pid, (err, kids) => { if (err) { reject(err); } if (kids) { spawn.sync("kill", [ `-${signal}`, this.process!.pid.toString(), ...kids, ]); } resolve(); }); }); } // This is called before watching starts. public onInit(): void { this.spawnProcess(); } // This is called before exiting. public async onCleanup() { this.restartDebounced.cancel(); if (this.isRunning) { await this.kill(); } } public execute(filePath: string) { if (this.isRunning) { this.restartDebounced(); return; } this.spawnProcess(); } }