feat: add dockerized 3proxy control plane backend
This commit is contained in:
117
server/lib/runtime.ts
Normal file
117
server/lib/runtime.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import type { RuntimePaths, RuntimeSnapshot } from './config';
|
||||
|
||||
export interface RuntimeController {
|
||||
getSnapshot(): RuntimeSnapshot;
|
||||
start(): Promise<RuntimeSnapshot>;
|
||||
restart(): Promise<RuntimeSnapshot>;
|
||||
reload(): Promise<RuntimeSnapshot>;
|
||||
}
|
||||
|
||||
export class ThreeProxyManager implements RuntimeController {
|
||||
private child: ChildProcess | null = null;
|
||||
private snapshot: RuntimeSnapshot = {
|
||||
status: 'idle',
|
||||
pid: null,
|
||||
startedAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly binaryPath: string,
|
||||
private readonly configPath: string,
|
||||
private readonly paths: RuntimePaths,
|
||||
private readonly autoStart: boolean,
|
||||
) {}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await fs.mkdir(path.dirname(this.paths.configPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(this.paths.counterPath), { recursive: true });
|
||||
await fs.mkdir(this.paths.reportDir, { recursive: true });
|
||||
await fs.mkdir(path.dirname(this.paths.logPath), { recursive: true });
|
||||
|
||||
if (this.autoStart) {
|
||||
await this.start();
|
||||
}
|
||||
}
|
||||
|
||||
getSnapshot(): RuntimeSnapshot {
|
||||
return { ...this.snapshot };
|
||||
}
|
||||
|
||||
async start(): Promise<RuntimeSnapshot> {
|
||||
if (this.child && this.child.exitCode === null) {
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
await ensureBinaryExists(this.binaryPath);
|
||||
|
||||
const child = spawn(this.binaryPath, [this.configPath], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
this.child = child;
|
||||
this.snapshot = {
|
||||
status: 'live',
|
||||
pid: child.pid ?? null,
|
||||
startedAt: new Date().toISOString(),
|
||||
lastError: null,
|
||||
};
|
||||
|
||||
child.stdout.on('data', (chunk) => process.stdout.write(`[3proxy] ${chunk}`));
|
||||
child.stderr.on('data', (chunk) => process.stderr.write(`[3proxy] ${chunk}`));
|
||||
child.on('exit', (code, signal) => {
|
||||
this.child = null;
|
||||
this.snapshot = {
|
||||
status: code === 0 || signal === 'SIGTERM' ? 'idle' : 'fail',
|
||||
pid: null,
|
||||
startedAt: null,
|
||||
lastError:
|
||||
code === 0 || signal === 'SIGTERM'
|
||||
? null
|
||||
: `3proxy exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'}).`,
|
||||
};
|
||||
});
|
||||
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
async restart(): Promise<RuntimeSnapshot> {
|
||||
await this.stop();
|
||||
return this.start();
|
||||
}
|
||||
|
||||
async reload(): Promise<RuntimeSnapshot> {
|
||||
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
||||
return this.start();
|
||||
}
|
||||
|
||||
process.kill(this.child.pid, 'SIGUSR1');
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
private async stop(): Promise<void> {
|
||||
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
||||
this.child = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.child;
|
||||
const waitForExit = new Promise<void>((resolve) => {
|
||||
current.once('exit', () => resolve());
|
||||
});
|
||||
|
||||
current.kill('SIGTERM');
|
||||
await waitForExit;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBinaryExists(binaryPath: string): Promise<void> {
|
||||
try {
|
||||
await fs.access(binaryPath);
|
||||
} catch {
|
||||
throw new Error(`3proxy binary not found at ${binaryPath}.`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user