const express = require('express'); const router = express.Router(); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs').promises; // 获取XNCore路径 function getXNCorePath() { const xnCorePath = process.env.XNCore || ''; if (!xnCorePath) { console.error('警告: 环境变量XNCore未设置'); return null; } return xnCorePath; } // 存储正在运行的仿真进程 const runningSimulations = new Map(); // 存储SSE连接 const sseClients = new Map(); // SSE中间件 function setupSSE(req, res) { // 设置SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); // 发送初始注释以保持连接 res.write(':\n\n'); // 定期发送注释以保持连接 const keepAliveId = setInterval(() => { res.write(':\n\n'); }, 15000); // 当客户端断开连接时清理 req.on('close', () => { clearInterval(keepAliveId); // 如果存在simulationId,从SSE客户端列表中移除 const simulationId = req.params.id; if (simulationId && sseClients.has(simulationId)) { sseClients.get(simulationId).delete(res); if (sseClients.get(simulationId).size === 0) { sseClients.delete(simulationId); } } }); return { res, keepAliveId }; } // 向SSE客户端发送消息 function sendSSEMessage(simulationId, event, data) { if (!sseClients.has(simulationId)) return; const clients = sseClients.get(simulationId); const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; for (const client of clients) { client.write(message); } } // 获取仿真状态的API router.get('/simulation-status/:id', (req, res) => { const simulationId = req.params.id; if (runningSimulations.has(simulationId)) { const simulation = runningSimulations.get(simulationId); res.json({ running: true, startTime: simulation.startTime, elapsedTime: Date.now() - simulation.startTime }); } else { res.json({ running: false, message: '仿真不存在或已结束' }); } }); // 执行仿真引擎 router.post('/run-simulation', async (req, res) => { try { const { args, id } = req.body; const simulationId = id || Date.now().toString(); if (!args || !Array.isArray(args)) { return res.status(400).json({ error: '缺少必要参数', message: '缺少args参数或格式不正确' }); } // 如果相同ID的仿真已经在运行,先终止它 if (runningSimulations.has(simulationId)) { const existingProcess = runningSimulations.get(simulationId); existingProcess.process.kill('SIGTERM'); runningSimulations.delete(simulationId); // 通知连接的客户端仿真被强制停止 sendSSEMessage(simulationId, 'terminated', { message: '仿真已被新的运行请求终止' }); } // 获取XNCore路径 const xnCorePath = getXNCorePath(); if (!xnCorePath) { return res.status(500).json({ error: 'XNCore未设置', message: '无法找到XNEngine可执行程序' }); } // 构建XNEngine路径 const enginePath = path.join(xnCorePath, 'XNEngine'); // 检查引擎程序是否存在 try { await fs.access(enginePath); } catch (error) { return res.status(404).json({ error: 'XNEngine不存在', message: `${enginePath} 不存在或无法访问` }); } // 直接使用用户提供的启动参数 // 启动仿真进程 - 设置stdio选项确保能捕获所有输出 const simProcess = spawn(enginePath, args, { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, LANG: 'zh_CN.UTF-8' } // 确保中文输出正确显示 }); let output = ''; let errorOutput = ''; // 收集标准输出,并推送到SSE simProcess.stdout.on('data', (data) => { const chunk = data.toString('utf8'); output += chunk; // 推送到SSE客户端 sendSSEMessage(simulationId, 'output', { type: 'stdout', data: chunk }); }); // 收集错误输出,并推送到SSE simProcess.stderr.on('data', (data) => { const chunk = data.toString('utf8'); errorOutput += chunk; // 推送到SSE客户端 sendSSEMessage(simulationId, 'output', { type: 'stderr', data: chunk }); }); // 设置超时(默认30秒,可由前端指定) const timeout = req.body.timeout || 30000; const timeoutId = setTimeout(() => { if (runningSimulations.has(simulationId)) { const simulation = runningSimulations.get(simulationId); if (simulation.process.exitCode === null) { simulation.process.kill('SIGTERM'); runningSimulations.delete(simulationId); // 推送超时事件到SSE客户端 sendSSEMessage(simulationId, 'timeout', { message: `仿真执行超过 ${timeout/1000} 秒,已自动终止` }); if (!simulation.hasResponded) { simulation.hasResponded = true; res.status(504).json({ error: '仿真超时', message: `仿真执行超过 ${timeout/1000} 秒`, output: output, errorOutput: errorOutput, simulationId: simulationId }); } } } }, timeout); // 保存到运行中的仿真Map runningSimulations.set(simulationId, { process: simProcess, timeoutId: timeoutId, startTime: Date.now(), output: output, errorOutput: errorOutput, hasResponded: false }); // 处理进程结束 simProcess.on('close', (code) => { if (runningSimulations.has(simulationId)) { const simulation = runningSimulations.get(simulationId); clearTimeout(simulation.timeoutId); // 推送完成事件到SSE客户端 sendSSEMessage(simulationId, 'completed', { exitCode: code, success: code === 0, message: code === 0 ? '仿真执行成功' : `仿真执行失败,退出码: ${code}` }); // 只有在尚未响应的情况下才发送响应 if (!simulation.hasResponded) { simulation.hasResponded = true; if (code === 0) { res.json({ success: true, message: '仿真执行成功', output: output, errorOutput: errorOutput, simulationId: simulationId }); } else { res.status(500).json({ error: '仿真执行失败', message: `仿真进程退出码: ${code}`, output: output, errorOutput: errorOutput, simulationId: simulationId }); } } // 从运行中的仿真Map移除 runningSimulations.delete(simulationId); } }); // 处理进程错误 simProcess.on('error', (error) => { if (runningSimulations.has(simulationId)) { const simulation = runningSimulations.get(simulationId); clearTimeout(simulation.timeoutId); // 推送错误事件到SSE客户端 sendSSEMessage(simulationId, 'error', { message: `启动仿真进程失败: ${error.message}` }); // 只有在尚未响应的情况下才发送响应 if (!simulation.hasResponded) { simulation.hasResponded = true; res.status(500).json({ error: '启动仿真进程失败', message: error.message, output: output, errorOutput: errorOutput, simulationId: simulationId }); } // 从运行中的仿真Map移除 runningSimulations.delete(simulationId); } }); // 立即响应,返回仿真ID,客户端可以通过SSE获取实时输出 if (!res.headersSent) { res.json({ success: true, message: '仿真已启动', simulationId: simulationId }); // 标记为已响应 if (runningSimulations.has(simulationId)) { runningSimulations.get(simulationId).hasResponded = true; } } } catch (error) { res.status(500).json({ error: '运行仿真失败', message: error.message }); } }); // 获取实时输出的SSE接口 router.get('/simulation-output/:id', (req, res) => { const simulationId = req.params.id; // 验证仿真ID if (!simulationId) { return res.status(400).json({ error: '缺少仿真ID' }); } // 初始化SSE连接 const { res: sseRes } = setupSSE(req, res); // 将该连接添加到SSE客户端列表 if (!sseClients.has(simulationId)) { sseClients.set(simulationId, new Set()); } sseClients.get(simulationId).add(sseRes); // 如果仿真已经在运行,发送初始状态 if (runningSimulations.has(simulationId)) { const simulation = runningSimulations.get(simulationId); // 发送已累积的输出 if (simulation.output) { sendSSEMessage(simulationId, 'output', { type: 'stdout', data: simulation.output }); } if (simulation.errorOutput) { sendSSEMessage(simulationId, 'output', { type: 'stderr', data: simulation.errorOutput }); } // 发送运行状态 sendSSEMessage(simulationId, 'status', { running: true, startTime: simulation.startTime, runTime: (Date.now() - simulation.startTime) / 1000 }); } else { // 发送仿真不存在或已结束的消息 sendSSEMessage(simulationId, 'status', { running: false, message: '仿真不存在或已结束' }); } }); // 终止仿真API router.post('/stop-simulation', (req, res) => { try { const { id } = req.body; if (!id) { return res.status(400).json({ error: '缺少必要参数', message: '缺少仿真ID' }); } // 检查仿真是否在运行 if (!runningSimulations.has(id)) { return res.status(404).json({ error: '仿真不存在', message: '没有找到指定ID的仿真' }); } // 获取仿真信息 const simulation = runningSimulations.get(id); // 终止仿真进程 simulation.process.kill('SIGTERM'); // 清除超时 clearTimeout(simulation.timeoutId); // 推送终止事件到SSE客户端 sendSSEMessage(id, 'terminated', { message: '仿真已手动终止' }); // 从运行中的仿真Map移除 runningSimulations.delete(id); // 计算运行时间 const runTime = (Date.now() - simulation.startTime) / 1000; // 响应结果 res.json({ success: true, message: `仿真已终止,运行时间: ${runTime.toFixed(2)}秒`, output: simulation.output, errorOutput: simulation.errorOutput }); } catch (error) { res.status(500).json({ error: '终止仿真失败', message: error.message }); } }); // 获取所有运行中的仿真 router.get('/running-simulations', (req, res) => { try { const simulations = []; for (const [id, simulation] of runningSimulations.entries()) { simulations.push({ id: id, startTime: simulation.startTime, runTime: (Date.now() - simulation.startTime) / 1000 }); } res.json({ count: simulations.length, simulations: simulations }); } catch (error) { res.status(500).json({ error: '获取运行中的仿真失败', message: error.message }); } }); module.exports = router;