433 lines
12 KiB
JavaScript
433 lines
12 KiB
JavaScript
|
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;
|