XNSim/XNSimHtml/routes/run-simulation.js
2025-04-28 12:25:20 +08:00

433 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;