import http, { IncomingMessage, ServerResponse } from "http"; import httpProxy from 'http-proxy'; import fs from "fs"; import child_process, { ChildProcess } from "child_process"; import kill from "tree-kill"; import axios from "axios"; let config = require("./config.json"); class ServiceManager { proxy = httpProxy.createProxy(); server = http.createServer((req, res) => this.listen(req, res)); services: Service[] = []; stopped: boolean = false; async setup() { this.server.listen(config.listen); } async stop() { if(this.stopped) return; this.stopped = true; this.services.forEach(s => s.stop()); if(config.socket) fs.rmSync(config.listen); } async listen(req: IncomingMessage, res: ServerResponse) { let oriHost = req.headers.host; let host = oriHost.indexOf(":") ? oriHost.split(':')[0] : oriHost; for(let i in this.services) { if(this.services[i].hosts.indexOf(host) !== -1) { if(this.services[i].socket) { this.proxy.web(req, res, {target: { socketPath: await this.services[i].start(), host: "localhost" }}); } else { this.proxy.web(req, res, {target: await this.services[i].start()}); } return; } } console.warn("Host", host, "not found!"); res.statusCode = 404; res.end(); } } class Service implements ServiceConfig { hosts: string[]; type: ServiceType; start_cmd: string; stop_cmd: string; listen: string; socket: boolean; waiting: boolean = false; waiting_clients: Function[] = []; child?: ChildProcess; timeout?: number; timeout_check?: NodeJS.Timeout; last_activity: Date; man: ServiceManager; alive: boolean = false; constructor(man: ServiceManager, config: ServiceConfig) { this.man = man; this.hosts = config.hosts; this.type = config.type; this.start_cmd = config.start_cmd; this.stop_cmd = config.stop_cmd; this.socket = config.socket; this.listen = config.listen; this.timeout = config.timeout ? config.timeout*1000 : 0; man.services.push(this); } async start() { this.last_activity = new Date; if(this.timeout && !this.waiting) { if(this.timeout_check) clearTimeout(this.timeout_check); this.timeout_check = setTimeout(() => { if((new Date).getTime() > this.last_activity.getTime()+this.timeout) this.stop(); }, this.timeout); } if(!this.child) { if(this.socket) if(fs.existsSync(this.listen)) fs.rmSync(this.listen); this.child = child_process.exec(this.start_cmd); console.log(`Started service ${this.hosts[0]}`); } if(!this.alive) { await this.life_check(); this.alive = true; } return this.listen; } async stop() { if(this.socket) if(fs.existsSync(this.listen)) fs.rmSync(this.listen); if(this.alive) { this.alive = false; if(this.type == "process") kill(this.child.pid, "SIGUSR2"); if(this.type == "control") child_process.exec(this.stop_cmd); this.child = null; console.log(`Stopped service ${this.hosts[0]}`); } } async life_check() { if(!this.waiting) { this.waiting = true; let done = () => { clearInterval(int); this.waiting = false; for(let i in this.waiting_clients) { this.waiting_clients[i](); } }; let int = setInterval(async () => { if(this.socket) { if(fs.existsSync(this.listen)) done(); } else { try { if([200, 301, 302].indexOf((await axios.get(this.listen)).status) !== -1) done(); } catch(e) {} } }, 100); } await new Promise((resolve) => { this.waiting_clients.push(resolve); }); } } type ServiceType = "control" | "process"; interface ServiceConfig { hosts: string[]; type: ServiceType; start_cmd: string; stop_cmd?: string; listen: string; socket: boolean; timeout?: number; }; let start = async (config: ServiceConfig[]) => { let man = new ServiceManager; await man.setup(); config.forEach(s => { new Service(man, s); }); let exit = async (e: any) => { if(e) console.log(e); man.stop(); process.exit(0); }; process.on('exit', exit); process.on('beforeExit', exit); process.on('SIGINT', exit); process.on('SIGUSR1', exit); process.on('SIGUSR2', exit); process.on('SIGTERM', exit); process.on('uncaughtException', console.error); }; start(config.map);