101 lines
2.8 KiB
JavaScript
101 lines
2.8 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { setGatewayInterception } from './gatewayRouting.js';
|
|
import { HarborError } from '../shared/errors.js';
|
|
|
|
export function createSingboxRuntime({ configPath, gateway = false, tproxyChain = '' }) {
|
|
let child = null;
|
|
let configHash = '';
|
|
let startedAt = null;
|
|
|
|
const state = () => ({ running: Boolean(child), startedAt });
|
|
|
|
async function stop() {
|
|
if (gateway) setGatewayInterception(false, tproxyChain);
|
|
if (!child) {
|
|
configHash = '';
|
|
startedAt = null;
|
|
return state();
|
|
}
|
|
|
|
const current = child;
|
|
child = null;
|
|
configHash = '';
|
|
startedAt = null;
|
|
await new Promise((resolve) => {
|
|
const timeout = setTimeout(() => {
|
|
current.kill('SIGKILL');
|
|
resolve();
|
|
}, 4000);
|
|
current.once('exit', () => {
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
});
|
|
current.kill('SIGTERM');
|
|
});
|
|
return state();
|
|
}
|
|
|
|
async function apply({ force = false } = {}) {
|
|
if (!fs.existsSync(configPath)) {
|
|
await stop();
|
|
return state();
|
|
}
|
|
|
|
const check = spawnSync('sing-box', ['check', '-c', configPath], { encoding: 'utf8' });
|
|
if (check.status !== 0) {
|
|
throw new HarborError('CONFIG_INVALID', {
|
|
cause: new Error((check.stderr || check.stdout || check.error?.message || 'sing-box check failed').trim()),
|
|
});
|
|
}
|
|
|
|
const nextHash = crypto.createHash('sha256').update(fs.readFileSync(configPath)).digest('hex');
|
|
if (!force && child && nextHash === configHash) return state();
|
|
|
|
await stop();
|
|
let current;
|
|
try {
|
|
current = spawn('sing-box', ['run', '-c', configPath], {
|
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
current.once('spawn', resolve);
|
|
current.once('error', reject);
|
|
});
|
|
} catch (cause) {
|
|
throw new HarborError('PROCESS_START_FAILED', { cause });
|
|
}
|
|
child = current;
|
|
configHash = nextHash;
|
|
startedAt = new Date().toISOString();
|
|
try {
|
|
if (gateway) setGatewayInterception(true, tproxyChain);
|
|
} catch (error) {
|
|
current.kill('SIGTERM');
|
|
child = null;
|
|
configHash = '';
|
|
startedAt = null;
|
|
throw new HarborError('PROCESS_START_FAILED', { cause: error });
|
|
}
|
|
current.once('exit', () => {
|
|
if (child !== current) return;
|
|
child = null;
|
|
configHash = '';
|
|
startedAt = null;
|
|
if (gateway) setGatewayInterception(false, tproxyChain);
|
|
});
|
|
return state();
|
|
}
|
|
|
|
return {
|
|
get running() { return Boolean(child); },
|
|
get startedAt() { return startedAt; },
|
|
refresh: async () => state(),
|
|
apply,
|
|
restart: () => apply({ force: true }),
|
|
stop,
|
|
shutdown: stop,
|
|
};
|
|
}
|