| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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'; |
|
|
| |
| const IPC_PATH = isWindows |
| ? '\\\\.\\pipe\\webai2api-supervisor' |
| : path.join(os.tmpdir(), 'webai2api-supervisor.sock'); |
|
|
| |
| const RESTART_DELAY = 1000; |
|
|
| |
| let restartArgs = null; |
|
|
| |
|
|
| |
| |
| |
| |
| |
| 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}`); |
| } |
|
|
| |
| |
| |
| |
| |
| function checkCommand(cmd) { |
| if (isWindows) return true; |
| const result = spawnSync('which', [cmd], { encoding: 'utf8' }); |
| return result.status === 0; |
| } |
|
|
| |
| |
| |
| |
| |
| 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'); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| function isDisplayAvailable(displayNum) { |
| const lockFile = `/tmp/.X${displayNum}-lock`; |
| const socketFile = `/tmp/.X11-unix/X${displayNum}`; |
| return !fs.existsSync(lockFile) && !fs.existsSync(socketFile); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
|
|
| let serverProcess = null; |
| let isRestarting = false; |
|
|
| |
| let vncInfo = { |
| enabled: false, |
| port: 5900, |
| display: ':99', |
| xvfbMode: false |
| }; |
|
|
| |
| |
| |
| function startIpcServer() { |
| |
| 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:')) { |
| |
| 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') { |
| |
| 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, |
| ]; |
|
|
| |
| |
| |
| |
| function startServer(extraArgs = []) { |
| const serverPath = path.join(process.cwd(), 'src', 'server', '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' |
| }); |
|
|
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| 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); |
| } |
|
|
| |
| const displayNum = findAvailableDisplay(50); |
| log('INFO', `正在启动 Xvfb 虚拟显示器 (显示号: :${displayNum})...`); |
|
|
| |
| 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')); |
| } |
|
|
| |
| |
| |
| |
| async function startVncServer(display) { |
| if (!checkCommand('x11vnc')) { |
| log('WARN', '未找到 x11vnc 命令,跳过 VNC 启动'); |
| return; |
| } |
|
|
| |
| 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; |
| }); |
|
|
| |
| 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', '主进程已启动'); |
|
|
| |
| if (hasXvfb && isLinux && !isInXvfb) { |
| startInXvfb(args); |
| return; |
| } |
|
|
| |
| vncInfo.xvfbMode = isInXvfb; |
|
|
| |
| if (isInXvfb && hasVnc) { |
| const display = process.env.DISPLAY || ':99'; |
| await startVncServer(display); |
| } |
|
|
| |
| startIpcServer(); |
|
|
| |
| 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); |
| }); |
|
|