diff --git a/.gitignore b/.gitignore index 2751e75..0faad47 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ build/ #log log/ +logs/ diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index 3d88fa4..3ee549c 100644 Binary files a/Release/database/XNSim.db and b/Release/database/XNSim.db differ diff --git a/XNSimHtml/components/run-simulation.js b/XNSimHtml/components/run-simulation.js index 07be87d..2847d7c 100644 --- a/XNSimHtml/components/run-simulation.js +++ b/XNSimHtml/components/run-simulation.js @@ -8,6 +8,8 @@ class RunSimulation extends HTMLElement { this.services = []; this.currentSimulationId = null; this.eventSource = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 3; } // 添加reactivate方法,用于从缓存中恢复时检查仿真状态 @@ -34,10 +36,42 @@ class RunSimulation extends HTMLElement { } } - // 如果有正在运行的仿真ID,尝试重新连接 - if (this.currentSimulationId) { - // 检查仿真是否仍在运行 - this.checkSimulationStatus(this.currentSimulationId); + // 检查是否有XNEngine进程在运行 + 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(); + 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() { + // 先加载场景文件列表 this.fetchScenarioFiles(); + // 然后渲染UI this.render(); + + // 最后检查是否有XNEngine进程在运行 + this.checkAndConnectToExistingSimulation(); } async fetchScenarioFiles() { @@ -400,11 +439,32 @@ class RunSimulation extends HTMLElement { const simulationId = responseData.simulationId || this.currentSimulationId; 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连接获取实时输出 this.connectToEventSource(simulationId); - this.showSuccess(`仿真已启动`); - } catch (error) { console.error('执行仿真失败:', error); this.showError('执行仿真失败: ' + error.message); @@ -459,13 +519,25 @@ class RunSimulation extends HTMLElement { this.eventSource.addEventListener('status', (event) => { try { const data = JSON.parse(event.data); + console.log('收到状态事件:', data); if (data.running === false) { // 仿真已经不存在或已结束 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 this.resetUIAfterCompletion(); + } else if (data.running === true) { + // 更新状态为已连接 + this.showSuccess('已连接到运行中的仿真'); } } catch (error) { console.error('处理状态事件失败:', error); @@ -476,6 +548,7 @@ class RunSimulation extends HTMLElement { this.eventSource.addEventListener('completed', (event) => { try { const data = JSON.parse(event.data); + console.log('收到完成事件:', data); // 添加日志 if (data.success) { this.showSuccess('仿真执行成功'); @@ -494,6 +567,7 @@ class RunSimulation extends HTMLElement { this.eventSource.addEventListener('error', (event) => { try { const data = JSON.parse(event.data); + console.log('收到错误事件:', data); // 添加日志 this.showError(`仿真错误: ${data.message}`); // 重置UI @@ -507,6 +581,7 @@ class RunSimulation extends HTMLElement { this.eventSource.addEventListener('terminated', (event) => { try { const data = JSON.parse(event.data); + console.log('收到终止事件:', data); // 添加日志 this.showMessage(`仿真已终止: ${data.message}`); // 在输出框中添加终止信息 @@ -524,6 +599,7 @@ class RunSimulation extends HTMLElement { this.eventSource.addEventListener('timeout', (event) => { try { const data = JSON.parse(event.data); + console.log('收到超时事件:', data); // 添加日志 this.showError(`仿真超时: ${data.message}`); // 在输出框中添加超时信息 @@ -537,16 +613,21 @@ class RunSimulation extends HTMLElement { } }); - // 连接错误 + // 修改连接错误处理 this.eventSource.onerror = (error) => { console.error('SSE连接错误:', error); - // 检查是否已经清理了资源 + // 检查连接状态 if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED - this.showError('实时输出连接已断开'); - - // 重置UI - this.resetUIAfterCompletion(); + // 如果还有重连次数,尝试重新连接 + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + this.showMessage(`连接断开,尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); + setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000); + } else { + this.showError('实时输出连接已断开'); + this.resetUIAfterCompletion(); + } } }; } @@ -577,8 +658,9 @@ class RunSimulation extends HTMLElement { // 关闭SSE连接 this.closeEventSource(); - // 清除当前仿真ID + // 清除当前仿真ID和重连计数 this.currentSimulationId = null; + this.reconnectAttempts = 0; } async stopSimulation() { @@ -878,6 +960,98 @@ class RunSimulation extends HTMLElement { const scenarioSelect = this.shadowRoot.querySelector('#scenario-select'); 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); \ No newline at end of file diff --git a/XNSimHtml/routes/run-simulation.js b/XNSimHtml/routes/run-simulation.js index 4a94d0c..f973f67 100644 --- a/XNSimHtml/routes/run-simulation.js +++ b/XNSimHtml/routes/run-simulation.js @@ -1,18 +1,18 @@ const express = require('express'); const router = express.Router(); -const { spawn } = require('child_process'); +const { spawn, exec } = 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 util = require('util'); +const execPromise = util.promisify(exec); +const { + getRunningXNEngineProcess, + saveXNEngineProcess, + updateXNEngineProcessStatus, + deleteXNEngineProcess, + getLatestRunningXNEngineProcess +} = require('../utils/db-utils'); +const { getXNCorePath } = require('../utils/file-utils'); // 存储正在运行的仿真进程 const runningSimulations = new Map(); @@ -22,412 +22,482 @@ 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); + // 设置SSE headers + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); - // 如果存在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 }; + // 发送初始注释以保持连接 + 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); - } + 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'); - - // 检查引擎程序是否存在 +// 检查进程是否在运行 +async function isProcessRunning(pid) { try { - await fs.access(enginePath); + process.kill(pid, 0); + return true; } catch (error) { - return res.status(404).json({ - error: 'XNEngine不存在', - message: `${enginePath} 不存在或无法访问` - }); + return false; } - - // 直接使用用户提供的启动参数 - // 启动仿真进程 - 设置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; - } +} + +// 检查进程是否为XNEngine +async function isXNEngineProcess(pid) { + try { + const { stdout } = await execPromise(`ps -p ${pid} -o comm=`); + return stdout.trim() === 'XNEngine'; + } catch (error) { + return false; } - - } 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); +router.get('/simulation-output/:id', async (req, res) => { + const simulationId = req.params.id; - // 发送已累积的输出 - if (simulation.output) { - sendSSEMessage(simulationId, 'output', { - type: 'stdout', - data: simulation.output - }); + // 验证仿真ID + if (!simulationId) { + return res.status(400).json({ error: '缺少仿真ID' }); } - if (simulation.errorOutput) { - sendSSEMessage(simulationId, 'output', { - type: 'stderr', - data: simulation.errorOutput - }); - } + // 初始化SSE连接 + const { res: sseRes } = setupSSE(req, res); - // 发送运行状态 - sendSSEMessage(simulationId, 'status', { - running: true, - startTime: simulation.startTime, - runTime: (Date.now() - simulation.startTime) / 1000 - }); - } else { - // 发送仿真不存在或已结束的消息 - sendSSEMessage(simulationId, 'status', { - running: false, - message: '仿真不存在或已结束' - }); - } + // 将该连接添加到SSE客户端列表 + if (!sseClients.has(simulationId)) { + sseClients.set(simulationId, new Set()); + } + sseClients.get(simulationId).add(sseRes); + + try { + // 从数据库获取进程信息 + const processInfo = await getRunningXNEngineProcess(simulationId); + + 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 -router.post('/stop-simulation', (req, res) => { - try { - const { id } = req.body; - - if (!id) { - return res.status(400).json({ - error: '缺少必要参数', - message: '缺少仿真ID' - }); +// 修改run-simulation路由 +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参数或格式不正确' + }); + } + + // 检查是否有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 - }); - } }); -// 获取所有运行中的仿真 -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 - }); +// 修改stop-simulation路由 +router.post('/stop-simulation', async (req, res) => { + try { + const { id } = req.body; + + if (!id) { + return res.status(400).json({ + error: '缺少必要参数', + message: '缺少仿真ID' + }); + } + + // 从数据库获取进程信息 + 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; \ No newline at end of file diff --git a/XNSimHtml/utils/db-utils.js b/XNSimHtml/utils/db-utils.js index b612906..9c413ac 100644 --- a/XNSimHtml/utils/db-utils.js +++ b/XNSimHtml/utils/db-utils.js @@ -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 = { getATAChapters, getModelsByChapterId, @@ -1560,5 +1744,10 @@ module.exports = { deleteTodo, getUsers, getSystemLogs, - addSystemLog + addSystemLog, + getRunningXNEngineProcess, + saveXNEngineProcess, + updateXNEngineProcessStatus, + deleteXNEngineProcess, + getLatestRunningXNEngineProcess }; \ No newline at end of file