Files
harbor-net/src/server/singboxRuntime.ts
T
dokril daec12e013
Build and Deploy Gateway / build-and-push (push) Failing after 14s
Build and Deploy Gateway / deploy (push) Has been skipped
Update Harbor client and gateway integration workflows
2026-08-19 18:16:10 +03:00

127 lines
3.6 KiB
TypeScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
import { setGatewayInterception } from './gatewayRouting.js';
import { HarborError } from '../shared/errors.js';
export function createSingboxRuntime({
configPath,
gateway = false,
tproxyChain = '',
}: {
configPath: string;
gateway?: boolean;
tproxyChain?: string;
}) {
let child: ChildProcess | null = null;
let configHash = '';
let startedAt: string | null = null;
const state = () => ({ running: Boolean(child), startedAt });
function checkConfig(config: unknown) {
const directory = fs.mkdtempSync(`${configPath}.check-`);
const candidatePath = `${directory}/config.json`;
try {
fs.writeFileSync(candidatePath, JSON.stringify(config));
const check = spawnSync('sing-box', ['check', '-c', candidatePath], { 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()),
});
}
return { valid: true };
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
}
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<void>((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: ChildProcess;
try {
current = spawn('sing-box', ['run', '-c', configPath], {
stdio: ['ignore', 'inherit', 'inherit'],
});
await new Promise<void>((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(),
checkConfig,
apply,
restart: () => apply({ force: true }),
stop,
shutdown: stop,
};
}