127 lines
3.4 KiB
TypeScript
127 lines
3.4 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>;
|
|
stop(): 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 stop(): Promise<RuntimeSnapshot> {
|
|
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
|
this.child = null;
|
|
this.snapshot = {
|
|
...this.snapshot,
|
|
status: 'idle',
|
|
pid: null,
|
|
startedAt: null,
|
|
lastError: null,
|
|
};
|
|
return this.getSnapshot();
|
|
}
|
|
|
|
const current = this.child;
|
|
const waitForExit = new Promise<void>((resolve) => {
|
|
current.once('exit', () => resolve());
|
|
});
|
|
|
|
current.kill('SIGTERM');
|
|
await waitForExit;
|
|
return this.getSnapshot();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
async function ensureBinaryExists(binaryPath: string): Promise<void> {
|
|
try {
|
|
await fs.access(binaryPath);
|
|
} catch {
|
|
throw new Error(`3proxy binary not found at ${binaryPath}.`);
|
|
}
|
|
}
|