增加了XNEngine的进程监听,确保引擎与前端分离

This commit is contained in:
jinchao 2025-05-14 16:07:13 +08:00
parent df3b7f7853
commit cc042dc497
5 changed files with 842 additions and 408 deletions

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ build/
#log #log
log/ log/
logs/

Binary file not shown.

View File

@ -8,6 +8,8 @@ class RunSimulation extends HTMLElement {
this.services = []; this.services = [];
this.currentSimulationId = null; this.currentSimulationId = null;
this.eventSource = null; this.eventSource = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3;
} }
// 添加reactivate方法用于从缓存中恢复时检查仿真状态 // 添加reactivate方法用于从缓存中恢复时检查仿真状态
@ -34,10 +36,42 @@ class RunSimulation extends HTMLElement {
} }
} }
// 如果有正在运行的仿真ID尝试重新连接 // 检查是否有XNEngine进程在运行
if (this.currentSimulationId) { try {
// 检查仿真是否仍在运行 const response = await fetch('/api/check-xnengine');
this.checkSimulationStatus(this.currentSimulationId); const data = await response.json();
if (data.running) {
this.showMessage('检测到正在运行的仿真,正在重新连接...');
// 更新UI以反映运行状态
const runButton = this.shadowRoot.querySelector('#run-button');
if (runButton) {
runButton.disabled = true;
runButton.textContent = '运行中...';
}
// 显示终止按钮
const stopButton = this.shadowRoot.querySelector('#stop-button');
if (stopButton) {
stopButton.style.display = 'block';
}
// 使用进程ID作为仿真ID重新连接
this.currentSimulationId = data.pid.toString();
this.connectToEventSource(this.currentSimulationId);
// 清空并初始化输出框
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML = '重新连接到运行中的仿真...\n';
} else {
// 没有运行的XNEngine进程重置UI
this.resetUIAfterCompletion();
}
} catch (error) {
console.error('检查XNEngine进程失败:', error);
this.showError('检查仿真状态失败: ' + error.message);
this.resetUIAfterCompletion();
} }
} }
@ -79,8 +113,13 @@ class RunSimulation extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
// 先加载场景文件列表
this.fetchScenarioFiles(); this.fetchScenarioFiles();
// 然后渲染UI
this.render(); this.render();
// 最后检查是否有XNEngine进程在运行
this.checkAndConnectToExistingSimulation();
} }
async fetchScenarioFiles() { async fetchScenarioFiles() {
@ -400,11 +439,32 @@ class RunSimulation extends HTMLElement {
const simulationId = responseData.simulationId || this.currentSimulationId; const simulationId = responseData.simulationId || this.currentSimulationId;
this.currentSimulationId = simulationId; this.currentSimulationId = simulationId;
// 如果是连接到现有进程,显示相应消息
if (responseData.isExisting) {
this.showMessage('已连接到运行中的仿真');
outputContent.innerHTML = '已连接到运行中的仿真...\n';
// 如果有配置文件路径,自动选择
if (responseData.scenarioFile) {
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
if (scenarioSelect) {
// 检查配置文件是否在列表中
const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === responseData.scenarioFile);
if (fileExists) {
scenarioSelect.value = responseData.scenarioFile;
this.currentScenario = responseData.scenarioFile;
// 更新模型和服务列表
this.updateModelAndServiceLists();
}
}
}
} else {
this.showSuccess(`仿真已启动`);
}
// 设置SSE连接获取实时输出 // 设置SSE连接获取实时输出
this.connectToEventSource(simulationId); this.connectToEventSource(simulationId);
this.showSuccess(`仿真已启动`);
} catch (error) { } catch (error) {
console.error('执行仿真失败:', error); console.error('执行仿真失败:', error);
this.showError('执行仿真失败: ' + error.message); this.showError('执行仿真失败: ' + error.message);
@ -459,13 +519,25 @@ class RunSimulation extends HTMLElement {
this.eventSource.addEventListener('status', (event) => { this.eventSource.addEventListener('status', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('收到状态事件:', data);
if (data.running === false) { if (data.running === false) {
// 仿真已经不存在或已结束 // 仿真已经不存在或已结束
this.showMessage(data.message || '仿真不存在或已结束'); this.showMessage(data.message || '仿真不存在或已结束');
// 如果是进程不存在,尝试重新连接
if (data.message === '仿真不存在或已结束' && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.showMessage(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
return;
}
// 重置UI // 重置UI
this.resetUIAfterCompletion(); this.resetUIAfterCompletion();
} else if (data.running === true) {
// 更新状态为已连接
this.showSuccess('已连接到运行中的仿真');
} }
} catch (error) { } catch (error) {
console.error('处理状态事件失败:', error); console.error('处理状态事件失败:', error);
@ -476,6 +548,7 @@ class RunSimulation extends HTMLElement {
this.eventSource.addEventListener('completed', (event) => { this.eventSource.addEventListener('completed', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('收到完成事件:', data); // 添加日志
if (data.success) { if (data.success) {
this.showSuccess('仿真执行成功'); this.showSuccess('仿真执行成功');
@ -494,6 +567,7 @@ class RunSimulation extends HTMLElement {
this.eventSource.addEventListener('error', (event) => { this.eventSource.addEventListener('error', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('收到错误事件:', data); // 添加日志
this.showError(`仿真错误: ${data.message}`); this.showError(`仿真错误: ${data.message}`);
// 重置UI // 重置UI
@ -507,6 +581,7 @@ class RunSimulation extends HTMLElement {
this.eventSource.addEventListener('terminated', (event) => { this.eventSource.addEventListener('terminated', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('收到终止事件:', data); // 添加日志
this.showMessage(`仿真已终止: ${data.message}`); this.showMessage(`仿真已终止: ${data.message}`);
// 在输出框中添加终止信息 // 在输出框中添加终止信息
@ -524,6 +599,7 @@ class RunSimulation extends HTMLElement {
this.eventSource.addEventListener('timeout', (event) => { this.eventSource.addEventListener('timeout', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
console.log('收到超时事件:', data); // 添加日志
this.showError(`仿真超时: ${data.message}`); this.showError(`仿真超时: ${data.message}`);
// 在输出框中添加超时信息 // 在输出框中添加超时信息
@ -537,16 +613,21 @@ class RunSimulation extends HTMLElement {
} }
}); });
// 连接错误 // 修改连接错误处理
this.eventSource.onerror = (error) => { this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error); console.error('SSE连接错误:', error);
// 检查是否已经清理了资源 // 检查连接状态
if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED
this.showError('实时输出连接已断开'); // 如果还有重连次数,尝试重新连接
if (this.reconnectAttempts < this.maxReconnectAttempts) {
// 重置UI this.reconnectAttempts++;
this.resetUIAfterCompletion(); this.showMessage(`连接断开,尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000);
} else {
this.showError('实时输出连接已断开');
this.resetUIAfterCompletion();
}
} }
}; };
} }
@ -577,8 +658,9 @@ class RunSimulation extends HTMLElement {
// 关闭SSE连接 // 关闭SSE连接
this.closeEventSource(); this.closeEventSource();
// 清除当前仿真ID // 清除当前仿真ID和重连计数
this.currentSimulationId = null; this.currentSimulationId = null;
this.reconnectAttempts = 0;
} }
async stopSimulation() { async stopSimulation() {
@ -878,6 +960,98 @@ class RunSimulation extends HTMLElement {
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select'); const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event)); scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event));
} }
// 添加新方法:检查并连接到已有的仿真
async checkAndConnectToExistingSimulation() {
try {
const response = await fetch('/api/check-xnengine');
const data = await response.json();
if (data.running) {
this.showMessage('检测到正在运行的仿真,正在重新连接...');
// 更新UI以反映运行状态
const runButton = this.shadowRoot.querySelector('#run-button');
if (runButton) {
runButton.disabled = true;
runButton.textContent = '运行中...';
}
// 显示终止按钮
const stopButton = this.shadowRoot.querySelector('#stop-button');
if (stopButton) {
stopButton.style.display = 'block';
}
// 使用进程ID作为仿真ID重新连接
this.currentSimulationId = data.pid.toString();
// 确保场景文件列表已加载
if (this.scenarioFiles.length === 0) {
await this.fetchScenarioFiles();
}
// 如果有配置文件路径,自动选择并加载内容
if (data.scenarioFile) {
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
if (scenarioSelect) {
// 检查配置文件是否在列表中
const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === data.scenarioFile);
if (fileExists) {
// 先设置当前场景
this.currentScenario = data.scenarioFile;
// 加载配置文件内容
try {
this.showMessage('加载配置文件内容...');
const response = await fetch(`/api/file-content?path=${encodeURIComponent(data.scenarioFile)}`);
if (!response.ok) {
throw new Error('无法获取配置文件内容');
}
const xmlContent = await response.text();
this.parseScenarioXML(xmlContent);
// 更新下拉框选择
scenarioSelect.value = data.scenarioFile;
// 更新模型和服务列表
this.updateModelAndServiceLists();
this.showMessage('配置文件加载完成');
} catch (error) {
console.error('加载配置文件失败:', error);
this.showError('加载配置文件失败: ' + error.message);
}
} else {
console.warn('配置文件不在列表中:', data.scenarioFile);
this.showError('配置文件不在列表中: ' + data.scenarioFile);
}
}
}
// 清空并初始化输出框
const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML = '重新连接到运行中的仿真...\n';
// 连接到SSE获取输出
this.connectToEventSource(this.currentSimulationId);
// 更新状态为已连接
this.showSuccess('已连接到运行中的仿真');
// 重置重连尝试次数
this.reconnectAttempts = 0;
} else {
// 如果没有运行中的仿真重置UI
this.resetUIAfterCompletion();
}
} catch (error) {
console.error('检查XNEngine进程失败:', error);
this.showError('检查仿真状态失败: ' + error.message);
this.resetUIAfterCompletion();
}
}
} }
customElements.define('run-simulation', RunSimulation); customElements.define('run-simulation', RunSimulation);

View File

@ -1,18 +1,18 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { spawn } = require('child_process'); const { spawn, exec } = require('child_process');
const path = require('path'); const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const util = require('util');
// 获取XNCore路径 const execPromise = util.promisify(exec);
function getXNCorePath() { const {
const xnCorePath = process.env.XNCore || ''; getRunningXNEngineProcess,
if (!xnCorePath) { saveXNEngineProcess,
console.error('警告: 环境变量XNCore未设置'); updateXNEngineProcessStatus,
return null; deleteXNEngineProcess,
} getLatestRunningXNEngineProcess
return xnCorePath; } = require('../utils/db-utils');
} const { getXNCorePath } = require('../utils/file-utils');
// 存储正在运行的仿真进程 // 存储正在运行的仿真进程
const runningSimulations = new Map(); const runningSimulations = new Map();
@ -22,412 +22,482 @@ const sseClients = new Map();
// SSE中间件 // SSE中间件
function setupSSE(req, res) { function setupSSE(req, res) {
// 设置SSE headers // 设置SSE headers
res.writeHead(200, { res.writeHead(200, {
'Content-Type': 'text/event-stream', 'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Connection': 'keep-alive' 'Connection': 'keep-alive'
}); });
// 发送初始注释以保持连接 // 发送初始注释以保持连接
res.write(':\n\n');
// 定期发送注释以保持连接
const keepAliveId = setInterval(() => {
res.write(':\n\n'); res.write(':\n\n');
}, 15000);
// 当客户端断开连接时清理 // 定期发送注释以保持连接
req.on('close', () => { const keepAliveId = setInterval(() => {
clearInterval(keepAliveId); res.write(':\n\n');
}, 15000);
// 如果存在simulationId从SSE客户端列表中移除 // 当客户端断开连接时清理
const simulationId = req.params.id; req.on('close', () => {
if (simulationId && sseClients.has(simulationId)) { clearInterval(keepAliveId);
sseClients.get(simulationId).delete(res);
if (sseClients.get(simulationId).size === 0) {
sseClients.delete(simulationId);
}
}
});
return { res, 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客户端发送消息 // 向SSE客户端发送消息
function sendSSEMessage(simulationId, event, data) { function sendSSEMessage(simulationId, event, data) {
if (!sseClients.has(simulationId)) return; if (!sseClients.has(simulationId)) return;
const clients = sseClients.get(simulationId); const clients = sseClients.get(simulationId);
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) { for (const client of clients) {
client.write(message); client.write(message);
} }
} }
// 获取仿真状态的API // 检查进程是否在运行
router.get('/simulation-status/:id', (req, res) => { async function isProcessRunning(pid) {
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 { try {
await fs.access(enginePath); process.kill(pid, 0);
return true;
} catch (error) { } catch (error) {
return res.status(404).json({ return false;
error: 'XNEngine不存在',
message: `${enginePath} 不存在或无法访问`
});
} }
}
// 直接使用用户提供的启动参数 // 检查进程是否为XNEngine
// 启动仿真进程 - 设置stdio选项确保能捕获所有输出 async function isXNEngineProcess(pid) {
const simProcess = spawn(enginePath, args, { try {
stdio: ['ignore', 'pipe', 'pipe'], const { stdout } = await execPromise(`ps -p ${pid} -o comm=`);
env: { ...process.env, LANG: 'zh_CN.UTF-8' } // 确保中文输出正确显示 return stdout.trim() === 'XNEngine';
}); } catch (error) {
return false;
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接口 // 获取实时输出的SSE接口
router.get('/simulation-output/:id', (req, res) => { router.get('/simulation-output/:id', async (req, res) => {
const simulationId = req.params.id; const simulationId = req.params.id;
// 验证仿真ID // 验证仿真ID
if (!simulationId) { if (!simulationId) {
return res.status(400).json({ error: '缺少仿真ID' }); 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) { // 初始化SSE连接
sendSSEMessage(simulationId, 'output', { const { res: sseRes } = setupSSE(req, res);
type: 'stderr',
data: simulation.errorOutput
});
}
// 发送运行状态 // 将该连接添加到SSE客户端列表
sendSSEMessage(simulationId, 'status', { if (!sseClients.has(simulationId)) {
running: true, sseClients.set(simulationId, new Set());
startTime: simulation.startTime, }
runTime: (Date.now() - simulation.startTime) / 1000 sseClients.get(simulationId).add(sseRes);
});
} else { try {
// 发送仿真不存在或已结束的消息 // 从数据库获取进程信息
sendSSEMessage(simulationId, 'status', { const processInfo = await getRunningXNEngineProcess(simulationId);
running: false,
message: '仿真不存在或已结束' if (processInfo) {
}); // 检查进程是否真的在运行且是XNEngine进程
} const isRunning = await isProcessRunning(processInfo.pid);
const isXNEngine = await isXNEngineProcess(processInfo.pid);
if (isRunning && isXNEngine) {
// 发送运行状态
sendSSEMessage(simulationId, 'status', {
running: true,
pid: processInfo.pid,
startTime: processInfo.start_time,
message: '已连接到运行中的XNEngine进程'
});
// 将进程添加到runningSimulations
runningSimulations.set(simulationId, {
pid: processInfo.pid,
startTime: new Date(processInfo.start_time).getTime(),
output: '',
errorOutput: '',
logFile: processInfo.log_file
});
// 使用tail命令来跟踪日志文件
const tailProcess = spawn('tail', ['-f', processInfo.log_file], {
stdio: ['ignore', 'pipe', 'pipe']
});
// 收集标准输出
tailProcess.stdout.on('data', (data) => {
const chunk = data.toString('utf8');
const simulation = runningSimulations.get(simulationId);
if (simulation) {
simulation.output += chunk;
}
// 推送到SSE客户端
sendSSEMessage(simulationId, 'output', {
type: 'stdout',
data: chunk
});
});
// 当客户端断开连接时清理
req.on('close', () => {
tailProcess.kill();
});
// 当tail进程结束时
tailProcess.on('close', (code) => {
runningSimulations.delete(simulationId);
sendSSEMessage(simulationId, 'status', {
running: false,
message: '仿真进程已结束'
});
});
return;
} else {
// 进程已结束或不是XNEngine进程删除数据库记录
await deleteXNEngineProcess(simulationId);
}
}
// 如果没有找到运行中的进程,发送仿真不存在或已结束的消息
sendSSEMessage(simulationId, 'status', {
running: false,
message: '仿真不存在或已结束'
});
} catch (error) {
console.error('处理SSE连接失败:', error);
sendSSEMessage(simulationId, 'status', {
running: false,
message: '连接失败: ' + error.message
});
}
}); });
// 终止仿真API // 修改run-simulation路由
router.post('/stop-simulation', (req, res) => { router.post('/run-simulation', async (req, res) => {
try { try {
const { id } = req.body; const { args, id } = req.body;
const simulationId = id || Date.now().toString();
if (!id) { if (!args || !Array.isArray(args)) {
return res.status(400).json({ return res.status(400).json({
error: '缺少必要参数', error: '缺少必要参数',
message: '缺少仿真ID' message: '缺少args参数或格式不正确'
}); });
}
// 检查是否有XNEngine进程在运行
try {
const { stdout } = await execPromise('ps -ef | grep XNEngine | grep -v grep || true');
if (stdout.trim()) {
const processes = stdout.trim().split('\n');
// 按启动时间排序,最早的进程通常是主进程
const sortedProcesses = processes.map(line => {
const parts = line.trim().split(/\s+/);
return {
pid: parts[1],
ppid: parts[2],
startTime: parts[4],
cmd: parts.slice(7).join(' ')
};
}).sort((a, b) => a.startTime.localeCompare(b.startTime));
// 找到主进程
const mainProcess = sortedProcesses[0];
// 检查主进程是否真的在运行且是XNEngine进程
const isRunning = await isProcessRunning(mainProcess.pid);
const isXNEngine = await isXNEngineProcess(mainProcess.pid);
if (isRunning && isXNEngine) {
// 检查数据库中是否已有该进程记录
const existingProcess = await getRunningXNEngineProcess(mainProcess.pid);
if (!existingProcess) {
// 创建日志文件
const logDir = path.join(process.cwd(), 'logs');
await fs.mkdir(logDir, { recursive: true });
const logFile = path.join(logDir, `xnengine_${mainProcess.pid}.log`);
// 从命令行参数中提取配置文件路径
const scenarioFile = args[0];
// 将进程信息写入数据库
await saveXNEngineProcess({
pid: mainProcess.pid,
log_file: logFile,
start_time: new Date().toISOString(),
cmd: mainProcess.cmd,
scenario_file: scenarioFile
});
}
// 返回成功响应
res.json({
success: true,
message: '已连接到运行中的仿真',
simulationId: mainProcess.pid.toString(),
isExisting: true,
startTime: mainProcess.startTime,
totalProcesses: sortedProcesses.length,
scenarioFile: (existingProcess && existingProcess.scenario_file) || args[0]
});
return;
} else {
// 进程已结束或不是XNEngine进程删除数据库记录
await deleteXNEngineProcess(mainProcess.pid);
}
}
} catch (error) {
console.error('检查XNEngine进程失败:', error);
}
// 如果没有找到运行中的进程,则启动新的仿真
// 获取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} 不存在或无法访问`
});
}
// 创建日志文件
const logDir = path.join(process.cwd(), 'logs');
await fs.mkdir(logDir, { recursive: true });
const logFile = path.join(logDir, `xnengine_${simulationId}.log`);
// 使用nohup启动进程将输出重定向到日志文件
const cmd = `nohup ${enginePath} ${args.join(' ')} > ${logFile} 2>&1 & echo $!`;
const { stdout: pid } = await execPromise(cmd);
const processId = parseInt(pid.trim());
// 等待一小段时间确保进程启动
await new Promise(resolve => setTimeout(resolve, 1000));
// 检查进程是否成功启动且是XNEngine进程
const isRunning = await isProcessRunning(processId);
const isXNEngine = await isXNEngineProcess(processId);
if (isRunning && isXNEngine) {
// 将进程信息写入数据库
await saveXNEngineProcess({
pid: processId,
log_file: logFile,
start_time: new Date().toISOString(),
cmd: `${enginePath} ${args.join(' ')}`,
scenario_file: args[0]
});
// 保存到运行中的仿真Map
runningSimulations.set(simulationId, {
pid: processId,
startTime: Date.now(),
output: '',
errorOutput: '',
logFile: logFile
});
// 立即响应返回仿真ID
res.json({
success: true,
message: '仿真已启动',
simulationId: processId.toString(),
scenarioFile: args[0]
});
// 启动一个后台任务来监控日志文件
const tailProcess = spawn('tail', ['-f', logFile], {
stdio: ['ignore', 'pipe', 'pipe']
});
// 收集输出
tailProcess.stdout.on('data', (data) => {
const chunk = data.toString('utf8');
const simulation = runningSimulations.get(simulationId);
if (simulation) {
simulation.output += chunk;
}
// 推送到SSE客户端
sendSSEMessage(simulationId, 'output', {
type: 'stdout',
data: chunk
});
});
// 当tail进程结束时
tailProcess.on('close', (code) => {
runningSimulations.delete(simulationId);
// 删除数据库中的进程记录
deleteXNEngineProcess(processId);
sendSSEMessage(simulationId, 'status', {
running: false,
message: '仿真进程已结束'
});
});
} else {
// 进程启动失败或不是XNEngine进程
await deleteXNEngineProcess(processId);
res.status(500).json({
error: '启动仿真失败',
message: '进程启动后立即退出或不是XNEngine进程'
});
}
} catch (error) {
res.status(500).json({
error: '运行仿真失败',
message: error.message
});
} }
// 检查仿真是否在运行
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
});
}
}); });
// 获取所有运行中的仿真 // 修改stop-simulation路由
router.get('/running-simulations', (req, res) => { router.post('/stop-simulation', async (req, res) => {
try { try {
const simulations = []; const { id } = req.body;
for (const [id, simulation] of runningSimulations.entries()) { if (!id) {
simulations.push({ return res.status(400).json({
id: id, error: '缺少必要参数',
startTime: simulation.startTime, message: '缺少仿真ID'
runTime: (Date.now() - simulation.startTime) / 1000 });
}); }
// 从数据库获取进程信息
const processInfo = await getRunningXNEngineProcess(id);
if (processInfo) {
// 检查进程是否在运行且是XNEngine进程
const isRunning = await isProcessRunning(processInfo.pid);
const isXNEngine = await isXNEngineProcess(processInfo.pid);
if (isRunning && isXNEngine) {
// 终止进程
try {
process.kill(processInfo.pid, 'SIGTERM');
// 等待进程终止
await new Promise(resolve => setTimeout(resolve, 1000));
// 检查进程是否真的终止了
const stillRunning = await isProcessRunning(processInfo.pid);
if (stillRunning) {
// 如果还在运行,强制终止
process.kill(processInfo.pid, 'SIGKILL');
}
// 删除数据库中的进程记录
await deleteXNEngineProcess(id);
// 推送终止事件到SSE客户端
sendSSEMessage(id, 'terminated', {
message: '仿真已终止'
});
// 从运行中的仿真Map移除
runningSimulations.delete(id);
res.json({
success: true,
message: '仿真已终止'
});
} catch (error) {
console.error('终止进程失败:', error);
res.status(500).json({
error: '终止仿真失败',
message: error.message
});
}
} else {
// 进程已经不在运行或不是XNEngine进程删除数据库记录
await deleteXNEngineProcess(id);
res.json({
success: true,
message: '仿真进程已经停止'
});
}
} else {
res.json({
success: true,
message: '没有找到运行中的仿真进程'
});
}
} catch (error) {
res.status(500).json({
error: '终止仿真失败',
message: error.message
});
} }
});
res.json({ // 修改check-xnengine路由
count: simulations.length, router.get('/check-xnengine', async (req, res) => {
simulations: simulations try {
}); // 从数据库获取最新的运行中进程
const processInfo = await getLatestRunningXNEngineProcess();
} catch (error) { if (processInfo) {
res.status(500).json({ // 检查进程是否真的在运行且是XNEngine进程
error: '获取运行中的仿真失败', const isRunning = await isProcessRunning(processInfo.pid);
message: error.message const isXNEngine = await isXNEngineProcess(processInfo.pid);
});
} if (isRunning && isXNEngine) {
res.json({
running: true,
pid: processInfo.pid,
startTime: processInfo.start_time,
cmd: processInfo.cmd,
message: 'XNEngine进程正在运行'
});
} else {
// 进程已结束或不是XNEngine进程删除数据库记录
await deleteXNEngineProcess(processInfo.pid);
res.json({
running: false,
message: 'XNEngine进程已停止'
});
}
} else {
res.json({
running: false,
message: '未找到运行中的XNEngine进程'
});
}
} catch (error) {
console.error('检查XNEngine进程失败:', error);
res.status(500).json({
error: '检查XNEngine进程失败',
message: error.message
});
}
}); });
module.exports = router; module.exports = router;

View File

@ -1534,6 +1534,190 @@ function addSystemLog(logData) {
} }
} }
// 获取运行中的XNEngine进程
function getRunningXNEngineProcess(pid) {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath, { readonly: true });
// 创建进程表(如果不存在)
db.prepare(`
CREATE TABLE IF NOT EXISTS xnengine_processes (
pid INTEGER PRIMARY KEY,
log_file TEXT,
start_time TEXT,
cmd TEXT,
status TEXT DEFAULT 'running',
scenario_file TEXT
)
`).run();
const process = db.prepare('SELECT * FROM xnengine_processes WHERE pid = ?').get(pid);
db.close();
return process;
} catch (error) {
console.error('获取XNEngine进程信息失败:', error);
throw error;
}
}
// 保存XNEngine进程信息
function saveXNEngineProcess(processData) {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath);
// 创建进程表(如果不存在)
db.prepare(`
CREATE TABLE IF NOT EXISTS xnengine_processes (
pid INTEGER PRIMARY KEY,
log_file TEXT,
start_time TEXT,
cmd TEXT,
status TEXT DEFAULT 'running',
scenario_file TEXT
)
`).run();
const result = db.prepare(`
INSERT INTO xnengine_processes (pid, log_file, start_time, cmd, scenario_file)
VALUES (?, ?, ?, ?, ?)
`).run(
processData.pid,
processData.log_file,
processData.start_time,
processData.cmd,
processData.scenario_file || null
);
db.close();
return {
success: true,
message: 'XNEngine进程信息保存成功'
};
} catch (error) {
console.error('保存XNEngine进程信息失败:', error);
throw error;
}
}
// 更新XNEngine进程状态
function updateXNEngineProcessStatus(pid, status) {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath);
const result = db.prepare(`
UPDATE xnengine_processes
SET status = ?
WHERE pid = ?
`).run(status, pid);
db.close();
return {
success: true,
message: 'XNEngine进程状态更新成功'
};
} catch (error) {
console.error('更新XNEngine进程状态失败:', error);
throw error;
}
}
// 删除XNEngine进程信息
function deleteXNEngineProcess(pid) {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath);
const result = db.prepare('DELETE FROM xnengine_processes WHERE pid = ?').run(pid);
db.close();
return {
success: true,
message: 'XNEngine进程信息删除成功'
};
} catch (error) {
console.error('删除XNEngine进程信息失败:', error);
throw error;
}
}
// 获取最新的运行中XNEngine进程
function getLatestRunningXNEngineProcess() {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath, { readonly: true });
const process = db.prepare(`
SELECT * FROM xnengine_processes
WHERE status = ?
ORDER BY start_time DESC
LIMIT 1
`).get('running');
db.close();
return process;
} catch (error) {
console.error('获取最新XNEngine进程信息失败:', error);
throw error;
}
}
module.exports = { module.exports = {
getATAChapters, getATAChapters,
getModelsByChapterId, getModelsByChapterId,
@ -1560,5 +1744,10 @@ module.exports = {
deleteTodo, deleteTodo,
getUsers, getUsers,
getSystemLogs, getSystemLogs,
addSystemLog addSystemLog,
getRunningXNEngineProcess,
saveXNEngineProcess,
updateXNEngineProcessStatus,
deleteXNEngineProcess,
getLatestRunningXNEngineProcess
}; };