webai2hf / supervisor.js
iyougame's picture
Sync from GitHub: 71b4e35efd6a40202ad9904ea8e1cdf66d0504ac
372e639 verified
/**
* @fileoverview Supervisor 进程管理器
* @description 负责管理 Xvfb 环境和子服务的生命周期
*
* 功能:
* - Linux 环境下启动 xvfb-run
* - 使用 child_process.spawn 启动 server.js
* - 监听 IPC 通道接收重启指令
* - 子进程崩溃时自动重启
*/
import { spawn, spawnSync } from 'child_process';
import net from 'net';
import os from 'os';
import path from 'path';
import fs from 'fs';
// ==================== 配置 ====================
const isWindows = os.platform() === 'win32';
// IPC 通道路径
const IPC_PATH = isWindows
? '\\\\.\\pipe\\webai2api-supervisor'
: path.join(os.tmpdir(), 'webai2api-supervisor.sock');
// 重启延迟(毫秒)
const RESTART_DELAY = 1000;
// 下次重启使用的参数(由 IPC 设置)
let restartArgs = null;
// ==================== 工具函数 ====================
/**
* 简单日志
* @param {string} level
* @param {string} message
*/
function log(level, message) {
const now = new Date();
const pad = (n, len = 2) => String(n).padStart(len, '0');
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
const levelTag = level === 'ERROR' ? 'ERRO' : level;
console.log(`${date} ${time} [${levelTag}] [看门狗] ${message}`);
}
/**
* 检查命令是否存在(Linux)
* @param {string} cmd
* @returns {boolean}
*/
function checkCommand(cmd) {
if (isWindows) return true;
const result = spawnSync('which', [cmd], { encoding: 'utf8' });
return result.status === 0;
}
/**
* 检查端口是否可用
* @param {number} port
* @returns {Promise<boolean>}
*/
function isPortAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port, '127.0.0.1');
});
}
/**
* 查找可用端口
* @param {number} startPort - 起始端口
* @param {number} maxTries - 最大尝试次数
* @returns {Promise<number|null>}
*/
async function findAvailablePort(startPort, maxTries = 10) {
for (let i = 0; i < maxTries; i++) {
const port = startPort + i;
if (await isPortAvailable(port)) {
return port;
}
}
return null;
}
/**
* 检查 Xvfb 显示号是否可用
* @param {number} displayNum
* @returns {boolean}
*/
function isDisplayAvailable(displayNum) {
const lockFile = `/tmp/.X${displayNum}-lock`;
const socketFile = `/tmp/.X11-unix/X${displayNum}`;
return !fs.existsSync(lockFile) && !fs.existsSync(socketFile);
}
/**
* 查找可用的显示号
* @param {number} startNum - 起始显示号
* @param {number} maxTries - 最大尝试次数
* @returns {number}
*/
function findAvailableDisplay(startNum = 50, maxTries = 50) {
for (let i = 0; i < maxTries; i++) {
const num = startNum + i;
if (isDisplayAvailable(num)) {
return num;
}
}
// 回退:使用随机显示号
return 50 + Math.floor(Math.random() * 50);
}
// ==================== IPC 服务器 ====================
let serverProcess = null;
let isRestarting = false;
// VNC 状态追踪
let vncInfo = {
enabled: false,
port: 5900,
display: ':99',
xvfbMode: false
};
/**
* 启动 IPC 服务器
*/
function startIpcServer() {
// 清理旧的 socket 文件(Linux)
if (!isWindows && fs.existsSync(IPC_PATH)) {
try {
fs.unlinkSync(IPC_PATH);
} catch { }
}
const ipcServer = net.createServer((socket) => {
socket.on('data', (data) => {
const command = data.toString().trim();
if (command === 'RESTART' || command.startsWith('RESTART:')) {
// 支持 RESTART:参数 格式
const extraArgs = command.includes(':') ? command.split(':')[1].split(' ').filter(Boolean) : [];
log('INFO', `收到 IPC 指令: RESTART${extraArgs.length ? ' (参数: ' + extraArgs.join(' ') + ')' : ''}`);
socket.write('OK\n');
socket.end();
restartServer(extraArgs);
} else if (command === 'STOP') {
log('INFO', '收到 IPC 指令: STOP');
socket.write('OK\n');
socket.end();
stopAll();
} else if (command === 'GET_VNC_INFO') {
// 返回 VNC 状态信息并关闭连接
socket.write(JSON.stringify(vncInfo) + '\n');
socket.end();
} else {
socket.write('UNKNOWN_COMMAND\n');
socket.end();
}
});
});
ipcServer.listen(IPC_PATH, () => {
log('INFO', `IPC 服务器已启动: ${IPC_PATH}`);
});
ipcServer.on('error', (err) => {
log('ERROR', `IPC 服务器错误: ${err.message}`);
});
return ipcServer;
}
// ==================== 子进程管理 ====================
// 不可恢复的退出码(不应自动重启)
const FATAL_EXIT_CODES = [
78, // 配置/依赖错误
];
/**
* 启动 server.js 子进程
* @param {string[]} [extraArgs] - 额外的命令行参数
*/
function startServer(extraArgs = []) {
const serverPath = path.join(process.cwd(), 'src', 'server', 'server.js');
// 检查 server.js 是否存在
if (!fs.existsSync(serverPath)) {
log('ERROR', `未找到 server.js: ${serverPath}`);
process.exit(1);
}
const args = [serverPath, ...extraArgs];
const env = {
...process.env,
SUPERVISOR_IPC: IPC_PATH
};
log('INFO', '正在启动子服务 (src/server/server.js)...');
serverProcess = spawn(process.execPath, args, {
cwd: process.cwd(),
env,
stdio: 'inherit' // 将子进程 stdio 直接输出到主控制台
});
serverProcess.on('exit', (code, signal) => {
if (isRestarting) {
log('INFO', '子服务已停止,准备重启...');
isRestarting = false;
// 如果有新参数,使用新参数;否则使用原参数
const argsToUse = restartArgs !== null ? restartArgs : extraArgs;
restartArgs = null; // 重置
setTimeout(() => startServer(argsToUse), RESTART_DELAY);
} else if (code !== 0 && code !== null) {
// 检查是否为不可恢复的错误
if (FATAL_EXIT_CODES.includes(code)) {
log('ERROR', `子服务因配置/依赖错误退出 (code: ${code}),不会自动重启`);
process.exit(code);
}
log('WARN', `子服务异常退出 (code: ${code}),将自动重启...`);
setTimeout(() => startServer(extraArgs), RESTART_DELAY);
} else {
log('INFO', '子服务已正常退出');
process.exit(0);
}
});
serverProcess.on('error', (err) => {
log('ERROR', `子服务启动失败: ${err.message}`);
process.exit(1);
});
}
/**
* 重启子服务
* @param {string[]} [newArgs] - 新的启动参数(将覆盖原有参数)
*/
function restartServer(newArgs = null) {
if (isRestarting) {
log('WARN', '重启已在进行中,忽略重复请求');
return;
}
isRestarting = true;
log('INFO', '正在重启子服务...');
// 如果提供了新参数,更新启动参数
if (newArgs !== null) {
restartArgs = newArgs;
}
if (serverProcess) {
serverProcess.kill('SIGTERM');
}
}
/**
* 停止所有服务
*/
function stopAll() {
log('INFO', '正在停止所有服务...');
if (serverProcess) {
serverProcess.kill('SIGTERM');
}
setTimeout(() => process.exit(0), 500);
}
// ==================== Xvfb 处理(Linux) ====================
/**
* 在 Xvfb 中启动
* @param {string[]} originalArgs - 原始命令行参数
*/
function startInXvfb(originalArgs) {
if (!checkCommand('xvfb-run')) {
log('ERROR', '未找到 xvfb-run 命令');
log('ERROR', '请先安装 Xvfb:');
log('ERROR', ' - Ubuntu/Debian: sudo apt install xvfb');
log('ERROR', ' - CentOS/RHEL: sudo dnf install xorg-x11-server-Xvfb');
process.exit(1);
}
// 查找可用的显示号(从 50 开始,避免与常用的冲突)
const displayNum = findAvailableDisplay(50);
log('INFO', `正在启动 Xvfb 虚拟显示器 (显示号: :${displayNum})...`);
// 移除 -xvfb 参数
const newArgs = originalArgs.filter(arg => arg !== '-xvfb');
const xvfbArgs = [
`--server-num=${displayNum}`,
'--server-args=-ac -screen 0 1366x768x24',
'env',
'XVFB_RUNNING=true',
`DISPLAY=:${displayNum}`,
process.argv[0],
process.argv[1],
...newArgs
];
const xvfbProcess = spawn('xvfb-run', xvfbArgs, {
stdio: 'inherit'
});
xvfbProcess.on('error', (err) => {
log('ERROR', `Xvfb 启动失败: ${err.message}`);
process.exit(1);
});
xvfbProcess.on('exit', (code) => {
process.exit(code || 0);
});
// 处理退出信号
process.on('SIGINT', () => xvfbProcess.kill('SIGTERM'));
process.on('SIGTERM', () => xvfbProcess.kill('SIGTERM'));
}
/**
* 启动 VNC 服务器
* @param {string} display - 显示器编号
*/
async function startVncServer(display) {
if (!checkCommand('x11vnc')) {
log('WARN', '未找到 x11vnc 命令,跳过 VNC 启动');
return;
}
// 查找可用的 VNC 端口(从 5900 开始)
const vncPort = await findAvailablePort(5900, 100);
if (!vncPort) {
log('ERROR', '无法找到可用的 VNC 端口 (5900-5999)');
return;
}
log('INFO', `正在启动 VNC 服务器 (端口: ${vncPort})...`);
const vncProcess = spawn('x11vnc', [
'-display', display,
'-rfbport', String(vncPort),
'-localhost',
'-nopw',
'-shared',
'-forever',
'-noxdamage',
'-norc',
'-geometry', '1366x768'
], {
stdio: 'ignore',
detached: false
});
vncProcess.on('error', (err) => {
log('WARN', `VNC 启动失败: ${err.message}`);
vncInfo.enabled = false;
});
vncProcess.on('exit', () => {
vncInfo.enabled = false;
});
// 更新 VNC 状态
vncInfo.enabled = true;
vncInfo.port = vncPort;
vncInfo.display = display;
log('INFO', `VNC 服务器已启动,端口: ${vncPort}`);
// 处理退出信号
process.on('SIGINT', () => vncProcess.kill('SIGTERM'));
process.on('SIGTERM', () => vncProcess.kill('SIGTERM'));
}
// ==================== 主入口 ====================
async function main() {
const args = process.argv.slice(2);
const hasXvfb = args.includes('-xvfb');
const hasVnc = args.includes('-vnc');
const isInXvfb = process.env.XVFB_RUNNING === 'true';
const isLinux = os.platform() === 'linux';
log('INFO', '主进程已启动');
// 处理 Xvfb 参数(仅 Linux)
if (hasXvfb && isLinux && !isInXvfb) {
startInXvfb(args);
return;
}
// 设置 xvfbMode 标识
vncInfo.xvfbMode = isInXvfb;
// 如果在 Xvfb 中运行,启动 VNC
if (isInXvfb && hasVnc) {
const display = process.env.DISPLAY || ':99';
await startVncServer(display);
}
// 启动 IPC 服务器
startIpcServer();
// 启动子服务(过滤掉 -xvfb 和 -vnc 参数)
const serverArgs = args.filter(arg => arg !== '-xvfb' && arg !== '-vnc');
startServer(serverArgs);
// 处理退出信号
process.on('SIGINT', stopAll);
process.on('SIGTERM', stopAll);
}
main().catch((err) => {
log('ERROR', `启动失败: ${err.message}`);
process.exit(1);
});