class RunSimulation extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.scenarioFiles = []; this.currentScenario = null; this.modelGroups = []; this.services = []; this.currentSimulationId = null; this.eventSource = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 3; } // 添加reactivate方法,用于从缓存中恢复时检查仿真状态 async reactivate() { // 确保先加载场景文件列表 await this.fetchScenarioFiles(); // 如果之前已经选择了配置文件,恢复配置文件下拉框的选中状态 if (this.currentScenario) { const scenarioSelect = this.shadowRoot.querySelector('#scenario-select'); if (scenarioSelect) { // 检查所选配置文件是否在列表中 const fileExists = Array.from(scenarioSelect.options).some(opt => opt.value === this.currentScenario); if (fileExists) { scenarioSelect.value = this.currentScenario; // 恢复模型和服务列表显示 this.updateModelAndServiceLists(); } else { console.warn('之前选择的配置文件不再可用:', this.currentScenario); this.currentScenario = null; } } } // 检查是否有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(); } } // 检查仿真状态 async checkSimulationStatus(simulationId) { try { const response = await fetch(`/api/simulation-status/${simulationId}`); 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'; } // 重新连接到事件源 this.connectToEventSource(simulationId); } else { // 仿真已经停止,重置UI this.showMessage('仿真已结束'); this.resetUIAfterCompletion(); } } catch (error) { console.error('检查仿真状态失败:', error); // 假设仿真已停止 this.resetUIAfterCompletion(); } } connectedCallback() { // 先加载场景文件列表 this.fetchScenarioFiles(); // 然后渲染UI this.render(); // 最后检查是否有XNEngine进程在运行 this.checkAndConnectToExistingSimulation(); } async fetchScenarioFiles() { try { const response = await fetch('/api/scenario-files'); if (!response.ok) { throw new Error('无法获取场景文件'); } this.scenarioFiles = await response.json(); this.updateScenarioDropdown(); } catch (error) { console.error('获取场景文件失败:', error); this.showError('获取场景文件失败: ' + error.message); } } updateScenarioDropdown() { const dropdown = this.shadowRoot.querySelector('#scenario-select'); if (!dropdown) return; // 清空现有选项 dropdown.innerHTML = ''; // 添加提示选项 const defaultOption = document.createElement('option'); defaultOption.value = ''; defaultOption.textContent = '-- 选择配置文件 --'; defaultOption.disabled = true; defaultOption.selected = true; dropdown.appendChild(defaultOption); // 添加场景文件选项 this.scenarioFiles.forEach(file => { const option = document.createElement('option'); option.value = file.path; option.textContent = file.name; dropdown.appendChild(option); }); } async onScenarioSelected(event) { const selectedScenarioPath = event.target.value; if (!selectedScenarioPath) { return; } try { this.showMessage('加载配置文件内容...'); // 使用file-content API获取XML内容 const response = await fetch(`/api/file-content?path=${encodeURIComponent(selectedScenarioPath)}`); if (!response.ok) { throw new Error('无法获取配置文件内容'); } const xmlContent = await response.text(); this.parseScenarioXML(xmlContent); this.currentScenario = selectedScenarioPath; this.updateModelAndServiceLists(); this.showMessage('配置文件加载完成'); } catch (error) { console.error('加载配置文件失败:', error); this.showError('加载配置文件失败: ' + error.message); } } parseScenarioXML(xmlContent) { try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, "text/xml"); // 解析模型组和模型 this.modelGroups = []; const modelGroupElements = xmlDoc.querySelectorAll('ModelGroup'); modelGroupElements.forEach(groupElem => { const groupName = groupElem.getAttribute('Name'); const models = []; groupElem.querySelectorAll('Model').forEach(modelElem => { models.push({ name: modelElem.getAttribute('Name'), className: modelElem.getAttribute('ClassName') }); }); this.modelGroups.push({ name: groupName, models: models }); }); // 解析服务 this.services = []; const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service'); serviceElements.forEach(serviceElem => { this.services.push({ name: serviceElem.getAttribute('Name'), className: serviceElem.getAttribute('ClassName') }); }); } catch (error) { console.error('解析XML失败:', error); throw new Error('解析XML失败: ' + error.message); } } updateModelAndServiceLists() { // 更新模型列表 const modelListContainer = this.shadowRoot.querySelector('#model-list'); modelListContainer.innerHTML = ''; this.modelGroups.forEach(group => { const groupItem = document.createElement('div'); groupItem.className = 'list-group'; const groupHeader = document.createElement('div'); groupHeader.className = 'list-group-header'; groupHeader.textContent = group.name; groupItem.appendChild(groupHeader); const modelsList = document.createElement('div'); modelsList.className = 'list-items'; group.models.forEach(model => { const modelItem = document.createElement('div'); modelItem.className = 'list-item'; modelItem.textContent = model.name; modelsList.appendChild(modelItem); }); groupItem.appendChild(modelsList); modelListContainer.appendChild(groupItem); }); // 更新服务列表 const serviceListContainer = this.shadowRoot.querySelector('#service-list'); serviceListContainer.innerHTML = ''; this.services.forEach(service => { const serviceItem = document.createElement('div'); serviceItem.className = 'list-item'; serviceItem.textContent = service.name; serviceListContainer.appendChild(serviceItem); }); } // ANSI终端颜色转换为HTML convertAnsiToHtml(text) { if (!text) return ''; // 映射ANSI颜色代码到CSS类名 const ansiToHtmlClass = { // 前景色 (30-37) 30: 'ansi-black', 31: 'ansi-red', 32: 'ansi-green', 33: 'ansi-yellow', 34: 'ansi-blue', 35: 'ansi-magenta', 36: 'ansi-cyan', 37: 'ansi-white', // 明亮前景色 (90-97) 90: 'ansi-bright-black', 91: 'ansi-bright-red', 92: 'ansi-bright-green', 93: 'ansi-bright-yellow', 94: 'ansi-bright-blue', 95: 'ansi-bright-magenta', 96: 'ansi-bright-cyan', 97: 'ansi-bright-white', // 背景色 (40-47) 40: 'ansi-bg-black', 41: 'ansi-bg-red', 42: 'ansi-bg-green', 43: 'ansi-bg-yellow', 44: 'ansi-bg-blue', 45: 'ansi-bg-magenta', 46: 'ansi-bg-cyan', 47: 'ansi-bg-white', // 文字样式 1: 'ansi-bold', 3: 'ansi-italic', 4: 'ansi-underline' }; // 替换ANSI转义序列 const parts = []; let currentSpan = null; let currentClasses = []; // 先分割ANSI序列和文本 const regex = /\x1b\[([\d;]*)m/g; let lastIndex = 0; let match; while ((match = regex.exec(text)) !== null) { // 添加匹配前的文本 if (match.index > lastIndex) { parts.push({ text: text.substring(lastIndex, match.index), classes: [...currentClasses] }); } // 处理ANSI代码 const codes = match[1].split(';').map(Number); // 重置所有样式 if (codes.includes(0) || codes.length === 0) { currentClasses = []; } else { // 添加样式 for (const code of codes) { if (ansiToHtmlClass[code]) { currentClasses.push(ansiToHtmlClass[code]); } } } lastIndex = regex.lastIndex; } // 添加最后一段文本 if (lastIndex < text.length) { parts.push({ text: text.substring(lastIndex), classes: [...currentClasses] }); } // 生成HTML let html = ''; for (const part of parts) { if (part.text) { if (part.classes.length > 0) { html += `${this.escapeHtml(part.text)}`; } else { html += this.escapeHtml(part.text); } } } return html; } // 转义HTML特殊字符 escapeHtml(text) { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } async runTest() { const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value; if (!selectedScenario) { this.showError('请先选择配置文件'); return; } try { this.showMessage('准备运行仿真...'); const runButton = this.shadowRoot.querySelector('#run-button'); runButton.disabled = true; runButton.textContent = '运行中...'; // 显示终止按钮 const stopButton = this.shadowRoot.querySelector('#stop-button'); stopButton.style.display = 'block'; // 清空并初始化输出框 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML = '开始执行仿真...\n'; // 关闭之前的EventSource连接 if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } // 保存当前仿真信息 this.currentSimulationId = Date.now().toString(); // 准备启动参数 const simulationArgs = ['-f', selectedScenario]; // 调用后端API执行仿真 const response = await fetch('/api/run-simulation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.currentSimulationId, args: simulationArgs, timeout: 120000 // 2分钟超时 }) }); if (!response.ok) { const errorData = await response.json(); outputContent.innerHTML += `\n错误: ${errorData.message || '仿真执行失败'}\n`; if (errorData.output) outputContent.innerHTML += this.convertAnsiToHtml(errorData.output); if (errorData.errorOutput) outputContent.innerHTML += this.convertAnsiToHtml(errorData.errorOutput); throw new Error(errorData.message || '仿真执行失败'); } const responseData = await response.json(); // 获取仿真ID 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); } catch (error) { console.error('执行仿真失败:', error); this.showError('执行仿真失败: ' + error.message); // 显示错误详情 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML += `\n\n执行错误: ${error.message}`; // 隐藏终止按钮并重置运行按钮 const stopButton = this.shadowRoot.querySelector('#stop-button'); stopButton.style.display = 'none'; const runButton = this.shadowRoot.querySelector('#run-button'); runButton.disabled = false; runButton.textContent = '运行仿真'; // 清除当前仿真ID和关闭SSE连接 this.closeEventSource(); this.currentSimulationId = null; } } // 连接到SSE事件源获取实时输出 connectToEventSource(simulationId) { // 关闭之前的连接 this.closeEventSource(); // 创建新的SSE连接 const url = `/api/simulation-output/${simulationId}`; this.eventSource = new EventSource(url); // 标准输出和错误输出 this.eventSource.addEventListener('output', (event) => { try { const data = JSON.parse(event.data); const outputContent = this.shadowRoot.querySelector('#output-content'); // 添加新输出并应用ANSI颜色 if (data.data) { const html = this.convertAnsiToHtml(data.data); outputContent.innerHTML += html; // 自动滚动到底部 outputContent.scrollTop = outputContent.scrollHeight; } } catch (error) { console.error('处理输出事件失败:', error); } }); // 仿真状态 this.eventSource.addEventListener('status', (event) => { try { const data = JSON.parse(event.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); } }); // 仿真完成 this.eventSource.addEventListener('completed', (event) => { try { const data = JSON.parse(event.data); if (data.success) { this.showSuccess('仿真执行成功'); } else { this.showError(`仿真执行失败: ${data.message}`); } // 重置UI this.resetUIAfterCompletion(); } catch (error) { console.error('处理完成事件失败:', error); } }); // 仿真错误 this.eventSource.addEventListener('error', (event) => { try { const data = JSON.parse(event.data); this.showError(`仿真错误: ${data.message}`); // 重置UI this.resetUIAfterCompletion(); } catch (error) { console.error('处理错误事件失败:', error); } }); // 仿真终止 this.eventSource.addEventListener('terminated', (event) => { try { const data = JSON.parse(event.data); this.showMessage(`仿真已终止: ${data.message}`); // 在输出框中添加终止信息 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML += `\n\n仿真已终止:${data.message}`; // 重置UI this.resetUIAfterCompletion(); } catch (error) { console.error('处理终止事件失败:', error); } }); // 仿真超时 this.eventSource.addEventListener('timeout', (event) => { try { const data = JSON.parse(event.data); this.showError(`仿真超时: ${data.message}`); // 在输出框中添加超时信息 const outputContent = this.shadowRoot.querySelector('#output-content'); outputContent.innerHTML += `\n\n仿真超时:${data.message}`; // 重置UI this.resetUIAfterCompletion(); } catch (error) { console.error('处理超时事件失败:', error); } }); // 修改连接错误处理 this.eventSource.onerror = (error) => { console.error('SSE连接错误:', error); // 检查连接状态 if (this.eventSource && this.eventSource.readyState === 2) { // CLOSED // 如果还有重连次数,尝试重新连接 if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; this.showMessage(`连接断开,尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); setTimeout(() => this.checkAndConnectToExistingSimulation(), 1000); } else { this.showError('实时输出连接已断开'); this.resetUIAfterCompletion(); } } }; } // 关闭SSE连接 closeEventSource() { if (this.eventSource) { // 通知后端清理tail进程 if (this.currentSimulationId) { fetch(`/api/cleanup-simulation/${this.currentSimulationId}`, { method: 'POST' }).catch(error => { console.error('清理仿真资源失败:', error); }); } this.eventSource.close(); this.eventSource = null; } } // 在仿真完成后重置UI resetUIAfterCompletion() { // 隐藏终止按钮 const stopButton = this.shadowRoot.querySelector('#stop-button'); if (stopButton) { stopButton.style.display = 'none'; } // 重置运行按钮 const runButton = this.shadowRoot.querySelector('#run-button'); if (runButton) { runButton.disabled = false; runButton.textContent = '运行仿真'; } // 关闭SSE连接 this.closeEventSource(); // 清除当前仿真ID和重连计数 this.currentSimulationId = null; this.reconnectAttempts = 0; } async stopSimulation() { if (!this.currentSimulationId) { this.showMessage('没有正在运行的仿真'); return; } try { this.showMessage('正在终止仿真...'); const response = await fetch('/api/stop-simulation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.currentSimulationId }) }); if (response.ok) { const result = await response.json(); this.showSuccess(`${result.message || '仿真已终止'}`); // 终止信息会通过SSE推送到UI // 此处不需要额外操作 } else { const errorData = await response.json(); this.showError(`终止仿真失败: ${errorData.message || '未知错误'}`); // 可能是SSE已断开,手动重置UI this.resetUIAfterCompletion(); } } catch (error) { console.error('终止仿真失败:', error); this.showError('终止仿真失败: ' + error.message); // 手动重置UI this.resetUIAfterCompletion(); } } showError(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'red'; } showMessage(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'blue'; } showSuccess(message) { const messageElement = this.shadowRoot.querySelector('#message'); messageElement.textContent = message; messageElement.style.color = 'green'; } render() { this.shadowRoot.innerHTML = `