From 0296daa268a500de04d2c40d7ac8018b254b6e11 Mon Sep 17 00:00:00 2001 From: jinchao <383321154@qq.com> Date: Thu, 29 May 2025 10:01:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E8=BF=90=E8=A1=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=92=8C=E8=BF=90=E8=A1=8C=E4=BB=BF=E7=9C=9F=E5=90=88=E4=BA=8C?= =?UTF-8?q?=E4=B8=BA=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Release/database/XNSim.db | Bin 1224704 -> 1224704 bytes XNSimHtml/components/content-area.js | 7 +- .../components/{run-test.js => run-sim.js} | 433 ++++++- XNSimHtml/components/run-simulation.js | 1075 ----------------- XNSimHtml/components/sub-toolbar.js | 5 - XNSimHtml/main.html | 13 +- 6 files changed, 434 insertions(+), 1099 deletions(-) rename XNSimHtml/components/{run-test.js => run-sim.js} (57%) delete mode 100644 XNSimHtml/components/run-simulation.js diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index 3081d587196d09200b28c7bbbf408ee9a3e35e33..f4350232a0fbef252f1ceba97af950d82ede08c5 100644 GIT binary patch delta 542 zcmZp8;MMTJYl1Z6o{2KfjC(dFEb(Vn;p>>p9^lTz*Rfeq!Ge#AlUbb6P|rxuM5TFB zT>GRr#_f~hn2Hv1G4dxc@F(yc<5Szp$1K6e+bG)h#-4+rk=Hg|ntysBH?#Qky5>LqQ-v! delta 334 zcmZp8;MMTJYl1Z6vWYUzjLSAAEb(Vn=9@H`J-~glqJj%w^Q5@;NpXzZC&e)pE#zY1 zZD8O};5)#lu$7Nlf{&GBU2+J1KQv5GXpUT5VHa?8xXStF$WNHZhyzmCH8=ah36duzW`q$ m?_8dDz_7T-)Bf%c7Z7s;F^J6z#C$-^55xl7-~AC3+6(~GR%A>7 diff --git a/XNSimHtml/components/content-area.js b/XNSimHtml/components/content-area.js index 9cb5906..8e7aa0f 100644 --- a/XNSimHtml/components/content-area.js +++ b/XNSimHtml/components/content-area.js @@ -111,11 +111,8 @@ class ContentArea extends HTMLElement { case 'service-development': contentElement = document.createElement('service-development'); break; - case 'run-test': - contentElement = document.createElement('run-test'); - break; - case 'run-simulation': - contentElement = document.createElement('run-simulation'); + case 'run-sim': + contentElement = document.createElement('run-sim'); break; case 'simulation-monitor': contentElement = document.createElement('simulation-monitor'); diff --git a/XNSimHtml/components/run-test.js b/XNSimHtml/components/run-sim.js similarity index 57% rename from XNSimHtml/components/run-test.js rename to XNSimHtml/components/run-sim.js index 3f2dc45..c355d33 100644 --- a/XNSimHtml/components/run-test.js +++ b/XNSimHtml/components/run-sim.js @@ -1,4 +1,4 @@ -class RunTest extends HTMLElement { +class RunSim extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); @@ -7,12 +7,17 @@ class RunTest extends HTMLElement { this.eventSource = null; this.logFileWatcher = null; this.logFilePollingInterval = null; + this.currentSimulationId = null; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 3; } connectedCallback() { this.render(); this.loadModelGroups(); this.loadServices(); + // 检查并连接到已有的仿真 + this.checkAndConnectToExistingSimulation(); } async loadModelGroups() { @@ -280,12 +285,298 @@ class RunTest extends HTMLElement { } } + // 连接到SSE事件源获取实时输出 + connectToEventSource(simulationId) { + // 关闭之前的连接 + if (this.eventSource) { + this.eventSource.close(); + } + + // 创建新的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.checkSimulationStatus(simulationId), 1000); + return; + } + + // 重置UI + this.resetSimulationButtons(); + } 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.resetSimulationButtons(); + } catch (error) { + console.error('处理完成事件失败:', error); + } + }); + + // 仿真错误 + this.eventSource.addEventListener('error', (event) => { + try { + const data = JSON.parse(event.data); + this.showError(`仿真错误: ${data.message}`); + + // 重置UI + this.resetSimulationButtons(); + } 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.resetSimulationButtons(); + } 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.checkSimulationStatus(this.currentSimulationId), 1000); + } else { + this.showError('实时输出连接已断开'); + this.resetSimulationButtons(); + } + } + }; + } + + // 检查仿真状态 + async checkSimulationStatus(simulationId) { + try { + const response = await fetch(`/api/check-process/${simulationId}`); + const data = await response.json(); + + if (data.running) { + // 仿真仍在运行,重新连接到事件源 + this.showMessage('重新连接到运行中的仿真...'); + this.connectToEventSource(simulationId); + } else { + // 仿真已经停止,重置UI + this.showMessage('仿真已结束'); + this.resetSimulationButtons(); + } + } catch (error) { + console.error('检查仿真状态失败:', error); + // 假设仿真已停止 + this.resetSimulationButtons(); + } + } + + async runSimulation() { + const savedSelection = localStorage.getItem('xnsim-selection'); + const selection = savedSelection ? JSON.parse(savedSelection) : {}; + const confID = selection.configurationId; + + try { + this.showMessage('准备运行仿真...'); + const runButton = this.shadowRoot.querySelector('#run-simulation-button'); + const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button'); + const stopButton = this.shadowRoot.querySelector('#stop-simulation-button'); + + runButton.disabled = true; + pauseButton.disabled = false; + stopButton.disabled = false; + runButton.textContent = '运行中...'; + + // 清空并初始化输出框 + const outputContent = this.shadowRoot.querySelector('#output-content'); + outputContent.innerHTML = '开始执行仿真...\n'; + + // 准备启动参数 + const simulationArgs = ['-id', confID]; + + // 调用后端API执行仿真 + const response = await fetch('/api/run-simulation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + args: simulationArgs + }) + }); + + 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; + + // 保存当前仿真ID + this.currentSimulationId = simulationId; + + // 建立SSE连接 + this.connectToEventSource(simulationId); + + // 根据启动结果更新UI + if (responseData.success) { + this.showSuccess('仿真已启动'); + } else { + this.showError(`仿真启动失败: ${responseData.message || '未知错误'}`); + this.resetSimulationButtons(); + } + + } catch (error) { + console.error('执行仿真失败:', error); + this.showError('执行仿真失败: ' + error.message); + + // 重置按钮状态 + this.resetSimulationButtons(); + + // 显示错误详情 + const outputContent = this.shadowRoot.querySelector('#output-content'); + outputContent.innerHTML += `\n\n执行错误: ${error.message}`; + } + } + + // 添加新方法:检查并连接到已有的仿真 + 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-simulation-button'); + const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button'); + const stopButton = this.shadowRoot.querySelector('#stop-simulation-button'); + + if (runButton) { + runButton.disabled = true; + runButton.textContent = '运行中...'; + } + + if (pauseButton) { + pauseButton.disabled = false; + } + + if (stopButton) { + stopButton.disabled = false; + } + + // 使用进程ID作为仿真ID重新连接 + this.currentSimulationId = data.pid.toString(); + + // 清空并初始化输出框 + const outputContent = this.shadowRoot.querySelector('#output-content'); + outputContent.innerHTML = '重新连接到运行中的仿真...\n'; + + // 连接到SSE获取输出 + this.connectToEventSource(this.currentSimulationId); + + // 更新状态为已连接 + this.showSuccess('已连接到运行中的仿真'); + + // 重置重连尝试次数 + this.reconnectAttempts = 0; + } else { + // 如果没有运行中的仿真,重置UI + this.resetSimulationButtons(); + } + } catch (error) { + console.error('检查XNEngine进程失败:', error); + this.showError('检查仿真状态失败: ' + error.message); + this.resetSimulationButtons(); + } + } + + // 添加reactivate方法,用于从缓存中恢复时检查仿真状态 + async reactivate() { + // 检查是否有XNEngine进程在运行 + await this.checkAndConnectToExistingSimulation(); + } + // 在组件销毁时清理资源 disconnectedCallback() { if (this.logFilePollingInterval) { clearInterval(this.logFilePollingInterval); this.logFilePollingInterval = null; } + if (this.eventSource) { + this.eventSource.close(); + this.eventSource = null; + } + this.currentSimulationId = null; + this.reconnectAttempts = 0; } showError(message) { @@ -477,6 +768,47 @@ class RunTest extends HTMLElement { background-color: #cccccc; cursor: not-allowed; } + + .button-group { + display: flex; + gap: 10px; + } + + .simulation-button { + background-color: #4caf50; + } + + .simulation-button:hover { + background-color: #45a049; + } + + .simulation-button:disabled { + background-color: #cccccc; + } + + .pause-button { + background-color: #ff9800; + } + + .pause-button:hover { + background-color: #f57c00; + } + + .pause-button:disabled { + background-color: #cccccc; + } + + .stop-button { + background-color: #f44336; + } + + .stop-button:hover { + background-color: #d32f2f; + } + + .stop-button:disabled { + background-color: #cccccc; + }
@@ -494,7 +826,12 @@ class RunTest extends HTMLElement {
运行输出
- +
+ + + + +
等待测试运行...
@@ -505,6 +842,15 @@ class RunTest extends HTMLElement { // 添加事件监听器 const runButton = this.shadowRoot.querySelector('#run-button'); runButton.addEventListener('click', () => this.runTest()); + + const runSimulationButton = this.shadowRoot.querySelector('#run-simulation-button'); + runSimulationButton.addEventListener('click', () => this.runSimulation()); + + const pauseSimulationButton = this.shadowRoot.querySelector('#pause-simulation-button'); + pauseSimulationButton.addEventListener('click', () => this.pauseSimulation()); + + const stopSimulationButton = this.shadowRoot.querySelector('#stop-simulation-button'); + stopSimulationButton.addEventListener('click', () => this.stopSimulation()); } // ANSI终端颜色转换为HTML @@ -618,6 +964,87 @@ class RunTest extends HTMLElement { .replace(/"/g, '"') .replace(/'/g, '''); } + + async pauseSimulation() { + const simulationId = this.currentSimulationId; + if (!simulationId) return; + + try { + const button = this.shadowRoot.querySelector('#pause-simulation-button'); + const isPaused = button.textContent === '继续仿真'; + + // 调用后端API暂停/继续仿真 + const response = await fetch('/api/pause-simulation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: simulationId, + pause: !isPaused + }) + }); + + if (!response.ok) { + throw new Error('操作失败'); + } + + // 更新按钮状态 + button.textContent = isPaused ? '暂停仿真' : '继续仿真'; + this.showMessage(isPaused ? '仿真已继续' : '仿真已暂停'); + } catch (error) { + console.error('暂停/继续仿真失败:', error); + this.showError('操作失败: ' + error.message); + } + } + + async stopSimulation() { + const simulationId = this.currentSimulationId; + if (!simulationId) return; + + try { + // 调用后端API停止仿真 + const response = await fetch('/api/stop-simulation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: simulationId + }) + }); + + if (!response.ok) { + throw new Error('停止仿真失败'); + } + + // 关闭SSE连接 + if (this.eventSource) { + this.eventSource.close(); + this.eventSource = null; + } + + // 重置按钮状态 + this.resetSimulationButtons(); + this.currentSimulationId = null; + this.showSuccess('仿真已停止'); + } catch (error) { + console.error('停止仿真失败:', error); + this.showError('停止仿真失败: ' + error.message); + } + } + + resetSimulationButtons() { + const runButton = this.shadowRoot.querySelector('#run-simulation-button'); + const pauseButton = this.shadowRoot.querySelector('#pause-simulation-button'); + const stopButton = this.shadowRoot.querySelector('#stop-simulation-button'); + + runButton.disabled = false; + runButton.textContent = '运行仿真'; + pauseButton.disabled = true; + stopButton.disabled = true; + pauseButton.textContent = '暂停仿真'; + } } -customElements.define('run-test', RunTest); \ No newline at end of file +customElements.define('run-sim', RunSim); \ No newline at end of file diff --git a/XNSimHtml/components/run-simulation.js b/XNSimHtml/components/run-simulation.js deleted file mode 100644 index f5228d7..0000000 --- a/XNSimHtml/components/run-simulation.js +++ /dev/null @@ -1,1075 +0,0 @@ -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 = ` - -
-
-
选择运行环境配置:
- -
- - -
-
-
-
-
-
-
模型列表
-
-
-
-
服务列表
-
-
-
-
-
运行输出
-
等待仿真运行...
-
-
-
- `; - - // 添加事件监听器 - const runButton = this.shadowRoot.querySelector('#run-button'); - runButton.addEventListener('click', () => this.runTest()); - - const stopButton = this.shadowRoot.querySelector('#stop-button'); - stopButton.addEventListener('click', () => this.stopSimulation()); - - 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(); - } - } - - // 添加组件销毁时的清理方法 - disconnectedCallback() { - // 清理SSE连接和相关资源 - this.closeEventSource(); - - // 重置所有状态 - this.currentSimulationId = null; - this.reconnectAttempts = 0; - this.scenarioFiles = []; - this.currentScenario = null; - this.modelGroups = []; - this.services = []; - } -} - -customElements.define('run-simulation', RunSimulation); \ No newline at end of file diff --git a/XNSimHtml/components/sub-toolbar.js b/XNSimHtml/components/sub-toolbar.js index c7fbbb1..b71c0ea 100644 --- a/XNSimHtml/components/sub-toolbar.js +++ b/XNSimHtml/components/sub-toolbar.js @@ -239,10 +239,6 @@ class SubToolbar extends HTMLElement {