Added control mode & more options

This commit is contained in:
Filip Znachor 2022-11-11 08:02:17 +01:00
parent 4d87ce2b83
commit a6a8232a7a
4 changed files with 208 additions and 37 deletions

View file

@ -1,12 +1,23 @@
{
"listen": "http.sock",
"socket": true,
"listen": "8080",
"socket": false,
"map": [
{
"hosts": ["test.example.com"],
"command": "node ../http/server.js",
"file": "http-server.sock",
"timeout": 60
"hosts": ["docker-service.local"],
"type": "control",
"start_cmd": "docker-compose -f /path/to/docker-compose.yml start",
"stop_cmd": "docker-compose -f /path/to/docker-compose.yml stop",
"socket": false,
"listen": "http://127.0.0.1:3000",
"timeout": 120
},
{
"hosts": ["node-webserver.local"],
"type": "process",
"start_cmd": "PORT=listen.sock node ./server/index.js",
"socket": true,
"listen": "listen.sock",
"timeout": 120
}
]
}

108
index.ts
View file

@ -3,6 +3,7 @@ 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");
@ -27,17 +28,20 @@ class ServiceManager {
let host = oriHost.indexOf(":") ? oriHost.split(':')[0] : oriHost;
for(let i in this.services) {
if(this.services[i].hosts.indexOf(host) !== -1) {
this.proxy.web(req, res, {
target: {
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.end("Host not found in host map");
res.statusCode = 404;
res.end();
}
}
@ -45,69 +49,111 @@ class ServiceManager {
class Service implements ServiceConfig {
hosts: string[];
command: string;
file: string;
type: ServiceType;
start_cmd: string;
stop_cmd: string;
listen: string;
socket: boolean;
waiting: boolean = false;
waiting_clients: Function[] = [];
child?: ChildProcess;
timeout?: number;
life_check?: NodeJS.Timeout;
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.command = config.command;
this.file = config.file;
this.timeout = config.timeout*1000;
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) {
if(this.life_check) clearTimeout(this.life_check);
this.life_check = setTimeout(() => {
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(fs.existsSync(this.file)) fs.rmSync(this.file);
this.child = child_process.exec(this.command);
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 new Promise((resolve) => {
let int = setInterval(() => {
if(fs.existsSync(this.file)) {
clearInterval(int);
resolve(null);
}
}, 100);
});
await this.life_check();
this.alive = true;
}
return this.file;
return this.listen;
}
async stop() {
if(fs.existsSync(this.file)) fs.rmSync(this.file);
if(this.socket) if(fs.existsSync(this.listen)) fs.rmSync(this.listen);
if(this.alive) {
this.alive = false;
kill(this.child.pid, "SIGUSR2");
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[];
command: string;
file: string;
type: ServiceType;
start_cmd: string;
stop_cmd?: string;
listen: string;
socket: boolean;
timeout?: number;
};

View file

@ -8,6 +8,7 @@
"license": "MIT",
"dependencies": {
"@types/node": "^17.0.41",
"axios": "^1.1.3",
"http-proxy": "^1.18.1",
"tree-kill": "^1.2.2"
},

113
pnpm-lock.yaml Normal file
View file

@ -0,0 +1,113 @@
lockfileVersion: 5.4
specifiers:
'@types/http-proxy': ^1.17.9
'@types/node': ^17.0.41
axios: ^1.1.3
http-proxy: ^1.18.1
tree-kill: ^1.2.2
dependencies:
'@types/node': 17.0.45
axios: 1.1.3
http-proxy: 1.18.1
tree-kill: 1.2.2
devDependencies:
'@types/http-proxy': 1.17.9
packages:
/@types/http-proxy/1.17.9:
resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
dependencies:
'@types/node': 17.0.45
dev: true
/@types/node/17.0.45:
resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
/asynckit/0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/axios/1.1.3:
resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
dependencies:
follow-redirects: 1.15.2
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
dev: false
/combined-stream/1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/delayed-stream/1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false
/follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/http-proxy/1.18.1:
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
engines: {node: '>=8.0.0'}
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.15.2
requires-port: 1.0.0
transitivePeerDependencies:
- debug
dev: false
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types/2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/proxy-from-env/1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/tree-kill/1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
dev: false