/** * Copyright 2013-2019 GenieACS Inc. * * This file is part of GenieACS. * * GenieACS is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * GenieACS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with GenieACS. If not, see . */ import { spawn, ChildProcess } from "child_process"; import * as crypto from "crypto"; import * as config from "./config"; import { Fault } from "./types"; import { ROOT_DIR } from "./config"; import * as logger from "./logger"; import readline from "readline"; const TIMEOUT = +config.get("EXT_TIMEOUT"); const processes: { [script: string]: ChildProcess } = {}; const jobs = new Map(); export function run(args: string[]): Promise<{ fault: Fault; value: any }> { return new Promise((resolve) => { const scriptName = args[0]; const id = crypto.randomBytes(8).toString("hex"); jobs.set(id, resolve); if (!processes[scriptName]) { const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], { stdio: ["ignore", "pipe", "pipe", "ipc"], }); processes[scriptName] = p; p.on("error", (err) => { if (processes[scriptName] === p) { if (jobs.delete(id)) { resolve({ fault: { code: err.name, message: err.message }, value: null, }); } // eslint-disable-next-line @typescript-eslint/no-floating-promises kill(processes[scriptName]); delete processes[scriptName]; } }); p.on("disconnect", () => { if (processes[scriptName] === p) delete processes[scriptName]; }); p.on("message", (message) => { const func = jobs.get(message[0]); if (func) { jobs.delete(message[0]); // Wait for any disconnect even to fire setTimeout(() => { func({ fault: message[1], value: message[2] }); }); } }); const rlstdout = readline.createInterface(p.stdout); rlstdout.on("line", (line) => { logger.info({ message: `Ext ${scriptName}(${p.pid}): ${line}` }); }); const rlstderr = readline.createInterface(p.stderr); rlstderr.on("line", (line) => { logger.warn({ message: `Ext ${scriptName}(${p.pid}): ${line}` }); }); } setTimeout(() => { if (jobs.delete(id)) { resolve({ fault: { code: "timeout", message: "Extension timed out" }, value: null, }); } }, TIMEOUT); if (!processes[scriptName].connected) return false; return processes[scriptName].send([id, args.slice(1)]); }); } function kill(process: ChildProcess): Promise { return new Promise((resolve) => { const timeToKill = Date.now() + 5000; process.kill(); const t = setInterval(() => { if (!process.connected) { clearInterval(t); resolve(); } else if (Date.now() > timeToKill) { process.kill("SIGKILL"); clearInterval(t); resolve(); } }, 100); }); } export async function killAll(): Promise { await Promise.all( Object.entries(processes).map(([k, p]) => { delete processes[k]; return kill(p); }) ); }