Files
3proxyUI/server/lib/runtime.ts

118 lines
3.2 KiB
TypeScript

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}.`);
}
}