增加了XNEngine的进程监听,确保引擎与前端分离
This commit is contained in:
parent
df3b7f7853
commit
cc042dc497
1
.gitignore
vendored
1
.gitignore
vendored
@ -37,3 +37,4 @@ build/
|
|||||||
|
|
||||||
#log
|
#log
|
||||||
log/
|
log/
|
||||||
|
logs/
|
||||||
|
Binary file not shown.
@ -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);
|
@ -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');
|
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
// 当客户端断开连接时清理
|
|
||||||
req.on('close', () => {
|
|
||||||
clearInterval(keepAliveId);
|
|
||||||
|
|
||||||
// 如果存在simulationId,从SSE客户端列表中移除
|
// 发送初始注释以保持连接
|
||||||
const simulationId = req.params.id;
|
res.write(':\n\n');
|
||||||
if (simulationId && sseClients.has(simulationId)) {
|
|
||||||
sseClients.get(simulationId).delete(res);
|
// 定期发送注释以保持连接
|
||||||
if (sseClients.get(simulationId).size === 0) {
|
const keepAliveId = setInterval(() => {
|
||||||
sseClients.delete(simulationId);
|
res.write(':\n\n');
|
||||||
}
|
}, 15000);
|
||||||
}
|
|
||||||
});
|
// 当客户端断开连接时清理
|
||||||
|
req.on('close', () => {
|
||||||
return { res, keepAliveId };
|
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客户端发送消息
|
// 向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} 不存在或无法访问`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 直接使用用户提供的启动参数
|
|
||||||
// 启动仿真进程 - 设置stdio选项确保能捕获所有输出
|
// 检查进程是否为XNEngine
|
||||||
const simProcess = spawn(enginePath, args, {
|
async function isXNEngineProcess(pid) {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
try {
|
||||||
env: { ...process.env, LANG: 'zh_CN.UTF-8' } // 确保中文输出正确显示
|
const { stdout } = await execPromise(`ps -p ${pid} -o comm=`);
|
||||||
});
|
return stdout.trim() === 'XNEngine';
|
||||||
|
} catch (error) {
|
||||||
let output = '';
|
return false;
|
||||||
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
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 发送已累积的输出
|
// 验证仿真ID
|
||||||
if (simulation.output) {
|
if (!simulationId) {
|
||||||
sendSSEMessage(simulationId, 'output', {
|
return res.status(400).json({ error: '缺少仿真ID' });
|
||||||
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) {
|
|
||||||
return res.status(400).json({
|
if (!args || !Array.isArray(args)) {
|
||||||
error: '缺少必要参数',
|
return res.status(400).json({
|
||||||
message: '缺少仿真ID'
|
error: '缺少必要参数',
|
||||||
});
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改check-xnengine路由
|
||||||
|
router.get('/check-xnengine', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// 从数据库获取最新的运行中进程
|
||||||
|
const processInfo = await getLatestRunningXNEngineProcess();
|
||||||
|
|
||||||
|
if (processInfo) {
|
||||||
|
// 检查进程是否真的在运行且是XNEngine进程
|
||||||
|
const isRunning = await isProcessRunning(processInfo.pid);
|
||||||
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
|
||||||
count: simulations.length,
|
|
||||||
simulations: simulations
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({
|
|
||||||
error: '获取运行中的仿真失败',
|
|
||||||
message: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
@ -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
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user