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; restart(): Promise; reload(): Promise; } 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 { 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 { 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 { await this.stop(); return this.start(); } async reload(): Promise { 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 { if (!this.child || this.child.exitCode !== null || !this.child.pid) { this.child = null; return; } const current = this.child; const waitForExit = new Promise((resolve) => { current.once('exit', () => resolve()); }); current.kill('SIGTERM'); await waitForExit; } } async function ensureBinaryExists(binaryPath: string): Promise { try { await fs.access(binaryPath); } catch { throw new Error(`3proxy binary not found at ${binaryPath}.`); } }