更新了运行测试页面

This commit is contained in:
jinchao 2025-05-29 09:26:44 +08:00
parent 7c5021958d
commit 5ae0ecae52
4 changed files with 322 additions and 290 deletions

Binary file not shown.

View File

@ -565,8 +565,8 @@ class OverviewPage extends HTMLElement {
<li>官方网站</li> <li>官方网站</li>
</ul> </ul>
<div class="help-links"> <div class="help-links">
<a href="#" class="help-link" id="qa-link">常见问题</a> <a href="#" class="help-link" id="qa-link">Q&A</a>
<a href="#" class="help-link" id="help-link">帮助文档</a> <a href="#" class="help-link" id="help-link">帮助</a>
</div> </div>
</div> </div>

View File

@ -2,169 +2,151 @@ class RunTest extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.scenarioFiles = [];
this.currentScenario = null;
this.modelGroups = []; this.modelGroups = [];
this.services = []; this.services = [];
this.eventSource = null; this.eventSource = null;
this.logFileWatcher = null;
this.logFilePollingInterval = null;
} }
connectedCallback() { connectedCallback() {
this.fetchScenarioFiles();
this.render(); this.render();
this.loadModelGroups();
this.loadServices();
} }
async fetchScenarioFiles() { async loadModelGroups() {
try { try {
const response = await fetch('/api/scenario-files'); const savedSelection = localStorage.getItem('xnsim-selection');
if (!response.ok) { const selection = savedSelection ? JSON.parse(savedSelection) : {};
throw new Error('无法获取场景文件'); const confID = selection.configurationId;
}
this.scenarioFiles = await response.json();
this.updateScenarioDropdown();
} catch (error) {
console.error('获取场景文件失败:', error);
this.showError('获取场景文件失败: ' + error.message);
}
}
updateScenarioDropdown() { if (!confID) {
const dropdown = this.shadowRoot.querySelector('#scenario-select'); this.showError('未选择构型');
if (!dropdown) return; 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); const response = await fetch(`/api/configurations/${confID}/model-groups`);
if (!response.ok) {
throw new Error('获取模型组失败');
}
const groups = await response.json();
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 = []; this.modelGroups = [];
const modelGroupElements = xmlDoc.querySelectorAll('ModelGroup'); for (const group of groups) {
modelGroupElements.forEach(groupElem => { const modelsResponse = await fetch(`/api/model-groups/${group.GroupID}/models`);
const groupName = groupElem.getAttribute('Name'); if (!modelsResponse.ok) {
const models = []; throw new Error(`获取模型组 ${group.GroupName} 的模型失败`);
}
groupElem.querySelectorAll('Model').forEach(modelElem => { const models = await modelsResponse.json();
models.push({
name: modelElem.getAttribute('Name'),
className: modelElem.getAttribute('ClassName')
});
});
this.modelGroups.push({ this.modelGroups.push({
name: groupName, name: group.GroupName,
models: models groupId: group.GroupID,
freq: group.Freq,
priority: group.Priority,
cpuAff: group.CPUAff,
models: models.map(model => ({
className: model.ClassName,
version: model.ModelVersion
}))
}); });
}); }
// 解析服务 this.updateModelList();
this.services = [];
const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service');
serviceElements.forEach(serviceElem => {
this.services.push({
name: serviceElem.getAttribute('Name'),
className: serviceElem.getAttribute('ClassName')
});
});
} catch (error) { } catch (error) {
console.error('解析XML失败:', error); console.error('加载模型组失败:', error);
throw new Error('解析XML失败: ' + error.message); this.showError('加载模型组失败: ' + error.message);
} }
} }
updateModelAndServiceLists() { async loadServices() {
// 更新模型列表 try {
const savedSelection = localStorage.getItem('xnsim-selection');
const selection = savedSelection ? JSON.parse(savedSelection) : {};
const confID = selection.configurationId;
if (!confID) {
this.showError('未选择构型');
return;
}
// 获取服务列表
const response = await fetch(`/api/configurations/${confID}/services`);
if (!response.ok) {
throw new Error('获取服务列表失败');
}
const services = await response.json();
this.services = services.map(service => ({
className: service.ClassName,
version: service.ServiceVersion
}));
this.updateServiceList();
} catch (error) {
console.error('加载服务失败:', error);
this.showError('加载服务失败: ' + error.message);
}
}
updateModelList() {
const modelListContainer = this.shadowRoot.querySelector('#model-list'); const modelListContainer = this.shadowRoot.querySelector('#model-list');
if (!modelListContainer) return;
modelListContainer.innerHTML = ''; modelListContainer.innerHTML = '';
this.modelGroups.forEach(group => { this.modelGroups.forEach(group => {
const groupItem = document.createElement('div'); const groupItem = document.createElement('div');
groupItem.className = 'list-group'; groupItem.className = 'list-group';
// 组名
const groupHeader = document.createElement('div'); const groupHeader = document.createElement('div');
groupHeader.className = 'list-group-header'; groupHeader.className = 'list-group-header';
groupHeader.textContent = group.name; groupHeader.textContent = group.name;
groupItem.appendChild(groupHeader); groupItem.appendChild(groupHeader);
// 组信息(以小字显示)
const groupInfo = document.createElement('div');
groupInfo.className = 'list-group-info';
groupInfo.textContent = `频率:${group.freq}.0 Hz / 优先级:${group.priority} / 亲和性:${group.cpuAff}`;
groupItem.appendChild(groupInfo);
const modelsList = document.createElement('div'); const modelsList = document.createElement('div');
modelsList.className = 'list-items'; modelsList.className = 'list-items';
group.models.forEach(model => { group.models.forEach(model => {
const modelItem = document.createElement('div'); const modelItem = document.createElement('div');
modelItem.className = 'list-item'; modelItem.className = 'list-item';
modelItem.textContent = model.name; modelItem.textContent = `${model.className} (v${model.version})`;
modelsList.appendChild(modelItem); modelsList.appendChild(modelItem);
}); });
groupItem.appendChild(modelsList); groupItem.appendChild(modelsList);
modelListContainer.appendChild(groupItem); modelListContainer.appendChild(groupItem);
}); });
}
// 更新服务列表 updateServiceList() {
const serviceListContainer = this.shadowRoot.querySelector('#service-list'); const serviceListContainer = this.shadowRoot.querySelector('#service-list');
if (!serviceListContainer) return;
serviceListContainer.innerHTML = ''; serviceListContainer.innerHTML = '';
this.services.forEach(service => { this.services.forEach(service => {
const serviceItem = document.createElement('div'); const serviceItem = document.createElement('div');
serviceItem.className = 'list-item'; serviceItem.className = 'list-item';
serviceItem.textContent = service.name; serviceItem.textContent = `${service.className} (v${service.version})`;
serviceListContainer.appendChild(serviceItem); serviceListContainer.appendChild(serviceItem);
}); });
} }
async runTest() { async runTest() {
const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value; const savedSelection = localStorage.getItem('xnsim-selection');
if (!selectedScenario) { const selection = savedSelection ? JSON.parse(savedSelection) : {};
this.showError('请先选择配置文件'); const confID = selection.configurationId;
return;
}
try { try {
this.showMessage('准备运行测试...'); this.showMessage('准备运行测试...');
@ -176,14 +158,8 @@ class RunTest extends HTMLElement {
const outputContent = this.shadowRoot.querySelector('#output-content'); const outputContent = this.shadowRoot.querySelector('#output-content');
outputContent.innerHTML = '开始执行测试...\n'; outputContent.innerHTML = '开始执行测试...\n';
// 关闭之前的EventSource连接
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
// 准备启动参数 // 准备启动参数
const simulationArgs = ['-f', selectedScenario, '-test']; const simulationArgs = ['-id', confID, '-test'];
// 调用后端API执行测试 // 调用后端API执行测试
const response = await fetch('/api/run-simulation', { const response = await fetch('/api/run-simulation', {
@ -207,8 +183,12 @@ class RunTest extends HTMLElement {
const responseData = await response.json(); const responseData = await response.json();
// 连接到SSE获取实时输出 // 获取进程ID和日志文件路径
this.connectToEventSource(responseData.simulationId); const processId = responseData.simulationId;
const logFile = responseData.logFile;
// 开始轮询日志文件
this.startLogFilePolling(logFile);
// 根据测试结果更新UI // 根据测试结果更新UI
if (responseData.success) { if (responseData.success) {
@ -232,92 +212,79 @@ class RunTest extends HTMLElement {
} }
} }
// 连接到SSE事件源获取实时输出 // 开始轮询日志文件
connectToEventSource(simulationId) { startLogFilePolling(logFile) {
// 关闭之前的连接 // 清除之前的轮询
if (this.eventSource) { if (this.logFilePollingInterval) {
this.eventSource.close(); clearInterval(this.logFilePollingInterval);
this.logFilePollingInterval = null;
} }
// 创建新的SSE连接 let lastPosition = 0;
const url = `/api/simulation-output/${simulationId}`; const outputContent = this.shadowRoot.querySelector('#output-content');
this.eventSource = new EventSource(url);
// 标准输出和错误输出 // 每100ms检查一次文件
this.eventSource.addEventListener('output', (event) => { this.logFilePollingInterval = setInterval(async () => {
try { try {
const data = JSON.parse(event.data); // 检查进程是否还在运行
const outputContent = this.shadowRoot.querySelector('#output-content'); const response = await fetch(`/api/check-process/${logFile.split('_')[1].split('.')[0]}`);
const data = await response.json();
// 添加新输出并应用ANSI颜色 if (!data.running) {
if (data.data) { // 进程已结束,读取剩余内容并停止轮询
const html = this.convertAnsiToHtml(data.data); const finalContent = await this.readLogFile(logFile, lastPosition);
outputContent.innerHTML += html; if (finalContent) {
outputContent.innerHTML += this.convertAnsiToHtml(finalContent);
outputContent.scrollTop = outputContent.scrollHeight;
}
// 自动滚动到底部 clearInterval(this.logFilePollingInterval);
this.logFilePollingInterval = null;
if (data.success) {
this.showSuccess('测试执行成功');
} else {
this.showError(`测试执行失败: ${data.message || '未知错误'}`);
}
return;
}
// 读取新的日志内容
const content = await this.readLogFile(logFile, lastPosition);
if (content) {
outputContent.innerHTML += this.convertAnsiToHtml(content);
outputContent.scrollTop = outputContent.scrollHeight; outputContent.scrollTop = outputContent.scrollHeight;
lastPosition += content.length;
} }
} catch (error) { } catch (error) {
console.error('处理输出事件失败:', error); console.error('读取日志文件失败:', error);
clearInterval(this.logFilePollingInterval);
this.logFilePollingInterval = null;
this.showError('读取日志文件失败: ' + error.message);
} }
}); }, 100);
// 仿真状态
this.eventSource.addEventListener('status', (event) => {
try {
const data = JSON.parse(event.data);
if (data.running === false) {
// 测试已经结束
this.showMessage(data.message || '测试已结束');
this.closeEventSource();
}
} 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}`);
}
this.closeEventSource();
} catch (error) {
console.error('处理完成事件失败:', error);
}
});
// 仿真错误
this.eventSource.addEventListener('error', (event) => {
try {
const data = JSON.parse(event.data);
this.showError(`测试错误: ${data.message}`);
this.closeEventSource();
} catch (error) {
console.error('处理错误事件失败:', error);
}
});
// 连接错误处理
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
this.showError('实时输出连接已断开');
this.closeEventSource();
};
} }
// 关闭SSE连接 // 读取日志文件
closeEventSource() { async readLogFile(logFile, startPosition) {
if (this.eventSource) { try {
this.eventSource.close(); const response = await fetch(`/api/read-log-file?file=${encodeURIComponent(logFile)}&position=${startPosition}`);
this.eventSource = null; if (!response.ok) {
throw new Error('读取日志文件失败');
}
const data = await response.json();
return data.content;
} catch (error) {
console.error('读取日志文件失败:', error);
return null;
}
}
// 在组件销毁时清理资源
disconnectedCallback() {
if (this.logFilePollingInterval) {
clearInterval(this.logFilePollingInterval);
this.logFilePollingInterval = null;
} }
} }
@ -361,52 +328,6 @@ class RunTest extends HTMLElement {
flex-direction: column; flex-direction: column;
} }
.config-selector {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 10px;
}
.selector-label {
min-width: 120px;
font-weight: bold;
color: #333;
}
select {
flex: 1;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
padding: 8px 16px;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
min-width: 100px;
}
button:hover {
background-color: #1565c0;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#message {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
.content-container { .content-container {
display: flex; display: flex;
flex: 1; flex: 1;
@ -415,7 +336,7 @@ class RunTest extends HTMLElement {
} }
.lists-container { .lists-container {
width: 250px; width: 320px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
@ -450,9 +371,14 @@ class RunTest extends HTMLElement {
.list-group-header { .list-group-header {
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin-bottom: 5px; margin-bottom: 2px;
padding-bottom: 3px; }
border-bottom: 1px solid #eee;
.list-group-info {
font-size: 0.85em;
color: #666;
margin-bottom: 8px;
padding-left: 2px;
} }
.list-items { .list-items {
@ -477,6 +403,15 @@ class RunTest extends HTMLElement {
padding: 10px; padding: 10px;
font-weight: bold; font-weight: bold;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
#message {
flex: 1;
text-align: center;
margin: 0 10px;
} }
.output-content { .output-content {
@ -490,47 +425,60 @@ class RunTest extends HTMLElement {
line-height: 1.4; line-height: 1.4;
} }
/* 终端颜色支持 */ /* ANSI颜色样式 */
.output-content .ansi-black { color: #000000; } .ansi-black { color: #000000; }
.output-content .ansi-red { color: #ff0000; } .ansi-red { color: #ff0000; }
.output-content .ansi-green { color: #00ff00; } .ansi-green { color: #00ff00; }
.output-content .ansi-yellow { color: #ffff00; } .ansi-yellow { color: #ffff00; }
.output-content .ansi-blue { color: #0000ff; } .ansi-blue { color: #0000ff; }
.output-content .ansi-magenta { color: #ff00ff; } .ansi-magenta { color: #ff00ff; }
.output-content .ansi-cyan { color: #00ffff; } .ansi-cyan { color: #00ffff; }
.output-content .ansi-white { color: #ffffff; } .ansi-white { color: #ffffff; }
.output-content .ansi-bright-black { color: #808080; } .ansi-bright-black { color: #666666; }
.output-content .ansi-bright-red { color: #ff5555; } .ansi-bright-red { color: #ff6666; }
.output-content .ansi-bright-green { color: #55ff55; } .ansi-bright-green { color: #66ff66; }
.output-content .ansi-bright-yellow { color: #ffff55; } .ansi-bright-yellow { color: #ffff66; }
.output-content .ansi-bright-blue { color: #5555ff; } .ansi-bright-blue { color: #6666ff; }
.output-content .ansi-bright-magenta { color: #ff55ff; } .ansi-bright-magenta { color: #ff66ff; }
.output-content .ansi-bright-cyan { color: #55ffff; } .ansi-bright-cyan { color: #66ffff; }
.output-content .ansi-bright-white { color: #ffffff; } .ansi-bright-white { color: #ffffff; }
.output-content .ansi-bg-black { background-color: #000000; } .ansi-bg-black { background-color: #000000; }
.output-content .ansi-bg-red { background-color: #ff0000; } .ansi-bg-red { background-color: #ff0000; }
.output-content .ansi-bg-green { background-color: #00ff00; } .ansi-bg-green { background-color: #00ff00; }
.output-content .ansi-bg-yellow { background-color: #ffff00; } .ansi-bg-yellow { background-color: #ffff00; }
.output-content .ansi-bg-blue { background-color: #0000ff; } .ansi-bg-blue { background-color: #0000ff; }
.output-content .ansi-bg-magenta { background-color: #ff00ff; } .ansi-bg-magenta { background-color: #ff00ff; }
.output-content .ansi-bg-cyan { background-color: #00ffff; } .ansi-bg-cyan { background-color: #00ffff; }
.output-content .ansi-bg-white { background-color: #ffffff; } .ansi-bg-white { background-color: #ffffff; }
.output-content .ansi-bold { font-weight: bold; } .ansi-bold { font-weight: bold; }
.output-content .ansi-italic { font-style: italic; } .ansi-italic { font-style: italic; }
.output-content .ansi-underline { text-decoration: underline; } .ansi-underline { text-decoration: underline; }
button {
padding: 6px 12px;
background-color: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
min-width: 80px;
font-size: 0.9em;
}
button:hover {
background-color: #1565c0;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style> </style>
<div class="test-container"> <div class="test-container">
<div class="config-selector">
<div class="selector-label">选择运行环境配置:</div>
<select id="scenario-select">
<option value="" disabled selected>-- 加载配置文件中... --</option>
</select>
<button id="run-button">运行测试</button>
</div>
<div id="message"></div>
<div class="content-container"> <div class="content-container">
<div class="lists-container"> <div class="lists-container">
<div class="list-section"> <div class="list-section">
@ -543,7 +491,11 @@ class RunTest extends HTMLElement {
</div> </div>
</div> </div>
<div class="output-container"> <div class="output-container">
<div class="output-header">运行输出</div> <div class="output-header">
<span>运行输出</span>
<div id="message"></div>
<button id="run-button">运行测试</button>
</div>
<div id="output-content" class="output-content">等待测试运行...</div> <div id="output-content" class="output-content">等待测试运行...</div>
</div> </div>
</div> </div>
@ -553,9 +505,6 @@ class RunTest extends HTMLElement {
// 添加事件监听器 // 添加事件监听器
const runButton = this.shadowRoot.querySelector('#run-button'); const runButton = this.shadowRoot.querySelector('#run-button');
runButton.addEventListener('click', () => this.runTest()); runButton.addEventListener('click', () => this.runTest());
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event));
} }
// ANSI终端颜色转换为HTML // ANSI终端颜色转换为HTML

View File

@ -2,7 +2,8 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const { spawn, exec } = 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');
const fsPromises = require('fs').promises;
const util = require('util'); const util = require('util');
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);
const { const {
@ -237,7 +238,7 @@ router.post('/run-simulation', async (req, res) => {
if (!existingProcess) { if (!existingProcess) {
// 创建日志文件 // 创建日志文件
const logDir = path.join(process.cwd(), 'logs'); const logDir = path.join(process.cwd(), 'logs');
await fs.mkdir(logDir, { recursive: true }); await fsPromises.mkdir(logDir, { recursive: true });
const logFile = path.join(logDir, `xnengine_${mainProcess.pid}.log`); const logFile = path.join(logDir, `xnengine_${mainProcess.pid}.log`);
// 从命令行参数中提取配置文件路径 // 从命令行参数中提取配置文件路径
@ -288,7 +289,7 @@ router.post('/run-simulation', async (req, res) => {
// 检查引擎程序是否存在 // 检查引擎程序是否存在
try { try {
await fs.access(enginePath); await fsPromises.access(enginePath);
} catch (error) { } catch (error) {
return res.status(404).json({ return res.status(404).json({
error: 'XNEngine不存在', error: 'XNEngine不存在',
@ -298,7 +299,7 @@ router.post('/run-simulation', async (req, res) => {
// 创建日志文件 // 创建日志文件
const logDir = path.join(process.cwd(), 'logs'); const logDir = path.join(process.cwd(), 'logs');
await fs.mkdir(logDir, { recursive: true }); await fsPromises.mkdir(logDir, { recursive: true });
const logFile = path.join(logDir, `xnengine_${simulationId}.log`); const logFile = path.join(logDir, `xnengine_${simulationId}.log`);
// 使用nohup启动进程将输出重定向到日志文件 // 使用nohup启动进程将输出重定向到日志文件
@ -504,4 +505,86 @@ router.post('/cleanup-simulation/:id', (req, res) => {
res.json({ success: true, message: '仿真资源已清理' }); res.json({ success: true, message: '仿真资源已清理' });
}); });
// 添加读取日志文件的路由
router.get('/read-log-file', async (req, res) => {
try {
const { file, position } = req.query;
if (!file) {
return res.status(400).json({ error: '缺少日志文件路径' });
}
const startPosition = parseInt(position) || 0;
// 读取文件
const stats = await fsPromises.stat(file);
if (stats.size < startPosition) {
// 文件被截断,从头开始读取
startPosition = 0;
}
// 使用二进制模式读取以保留ANSI颜色代码
const stream = fs.createReadStream(file, {
start: startPosition,
encoding: null // 使用二进制模式
});
let content = '';
for await (const chunk of stream) {
// 将二进制数据转换为字符串保留ANSI颜色代码
content += chunk.toString('utf8');
}
res.json({ content });
} catch (error) {
console.error('读取日志文件失败:', error);
res.status(500).json({
error: '读取日志文件失败',
message: error.message
});
}
});
// 添加检查进程状态的路由
router.get('/check-process/:pid', async (req, res) => {
try {
const pid = req.params.pid;
if (!pid) {
return res.status(400).json({ error: '缺少进程ID' });
}
// 检查进程是否在运行
const isRunning = await isProcessRunning(pid);
const isXNEngine = await isXNEngineProcess(pid);
if (isRunning && isXNEngine) {
res.json({ running: true });
} else {
// 进程已结束,检查日志文件是否有错误
const logFile = path.join(process.cwd(), 'logs', `xnengine_${pid}.log`);
try {
const content = await fsPromises.readFile(logFile, 'utf8');
const hasError = content.includes('Error:') || content.includes('error:');
res.json({
running: false,
success: !hasError,
message: hasError ? '测试执行失败' : '测试执行成功'
});
} catch (error) {
res.json({
running: false,
success: false,
message: '无法读取日志文件'
});
}
}
} catch (error) {
console.error('检查进程状态失败:', error);
res.status(500).json({
error: '检查进程状态失败',
message: error.message
});
}
});
module.exports = router; module.exports = router;