更新了运行测试页面
This commit is contained in:
parent
7c5021958d
commit
5ae0ecae52
Binary file not shown.
@ -565,8 +565,8 @@ class OverviewPage extends HTMLElement {
|
||||
<li>官方网站:无</li>
|
||||
</ul>
|
||||
<div class="help-links">
|
||||
<a href="#" class="help-link" id="qa-link">常见问题</a>
|
||||
<a href="#" class="help-link" id="help-link">帮助文档</a>
|
||||
<a href="#" class="help-link" id="qa-link">Q&A</a>
|
||||
<a href="#" class="help-link" id="help-link">帮助</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -2,169 +2,151 @@ class RunTest extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.scenarioFiles = [];
|
||||
this.currentScenario = null;
|
||||
this.modelGroups = [];
|
||||
this.services = [];
|
||||
this.eventSource = null;
|
||||
this.logFileWatcher = null;
|
||||
this.logFilePollingInterval = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.fetchScenarioFiles();
|
||||
this.render();
|
||||
this.loadModelGroups();
|
||||
this.loadServices();
|
||||
}
|
||||
|
||||
async fetchScenarioFiles() {
|
||||
async loadModelGroups() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
const savedSelection = localStorage.getItem('xnsim-selection');
|
||||
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
||||
const confID = selection.configurationId;
|
||||
|
||||
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) {
|
||||
if (!confID) {
|
||||
this.showError('未选择构型');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.showMessage('加载配置文件内容...');
|
||||
// 使用file-content API获取XML内容
|
||||
const response = await fetch(`/api/file-content?path=${encodeURIComponent(selectedScenarioPath)}`);
|
||||
|
||||
// 获取模型组列表
|
||||
const response = await fetch(`/api/configurations/${confID}/model-groups`);
|
||||
if (!response.ok) {
|
||||
throw new Error('无法获取配置文件内容');
|
||||
throw new Error('获取模型组失败');
|
||||
}
|
||||
const groups = await response.json();
|
||||
|
||||
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')
|
||||
});
|
||||
});
|
||||
for (const group of groups) {
|
||||
const modelsResponse = await fetch(`/api/model-groups/${group.GroupID}/models`);
|
||||
if (!modelsResponse.ok) {
|
||||
throw new Error(`获取模型组 ${group.GroupName} 的模型失败`);
|
||||
}
|
||||
const models = await modelsResponse.json();
|
||||
|
||||
this.modelGroups.push({
|
||||
name: groupName,
|
||||
models: models
|
||||
});
|
||||
name: group.GroupName,
|
||||
groupId: group.GroupID,
|
||||
freq: group.Freq,
|
||||
priority: group.Priority,
|
||||
cpuAff: group.CPUAff,
|
||||
models: models.map(model => ({
|
||||
className: model.ClassName,
|
||||
version: model.ModelVersion
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// 解析服务
|
||||
this.services = [];
|
||||
const serviceElements = xmlDoc.querySelectorAll('ServicesList > Service');
|
||||
serviceElements.forEach(serviceElem => {
|
||||
this.services.push({
|
||||
name: serviceElem.getAttribute('Name'),
|
||||
className: serviceElem.getAttribute('ClassName')
|
||||
});
|
||||
});
|
||||
this.updateModelList();
|
||||
} catch (error) {
|
||||
console.error('解析XML失败:', error);
|
||||
throw new Error('解析XML失败: ' + error.message);
|
||||
console.error('加载模型组失败:', error);
|
||||
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');
|
||||
if (!modelListContainer) return;
|
||||
|
||||
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 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');
|
||||
modelsList.className = 'list-items';
|
||||
|
||||
group.models.forEach(model => {
|
||||
const modelItem = document.createElement('div');
|
||||
modelItem.className = 'list-item';
|
||||
modelItem.textContent = model.name;
|
||||
modelItem.textContent = `${model.className} (v${model.version})`;
|
||||
modelsList.appendChild(modelItem);
|
||||
});
|
||||
|
||||
groupItem.appendChild(modelsList);
|
||||
modelListContainer.appendChild(groupItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新服务列表
|
||||
updateServiceList() {
|
||||
const serviceListContainer = this.shadowRoot.querySelector('#service-list');
|
||||
if (!serviceListContainer) return;
|
||||
|
||||
serviceListContainer.innerHTML = '';
|
||||
|
||||
this.services.forEach(service => {
|
||||
const serviceItem = document.createElement('div');
|
||||
serviceItem.className = 'list-item';
|
||||
serviceItem.textContent = service.name;
|
||||
serviceItem.textContent = `${service.className} (v${service.version})`;
|
||||
serviceListContainer.appendChild(serviceItem);
|
||||
});
|
||||
}
|
||||
|
||||
async runTest() {
|
||||
const selectedScenario = this.shadowRoot.querySelector('#scenario-select').value;
|
||||
if (!selectedScenario) {
|
||||
this.showError('请先选择配置文件');
|
||||
return;
|
||||
}
|
||||
const savedSelection = localStorage.getItem('xnsim-selection');
|
||||
const selection = savedSelection ? JSON.parse(savedSelection) : {};
|
||||
const confID = selection.configurationId;
|
||||
|
||||
try {
|
||||
this.showMessage('准备运行测试...');
|
||||
@ -176,14 +158,8 @@ class RunTest extends HTMLElement {
|
||||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||||
outputContent.innerHTML = '开始执行测试...\n';
|
||||
|
||||
// 关闭之前的EventSource连接
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
|
||||
// 准备启动参数
|
||||
const simulationArgs = ['-f', selectedScenario, '-test'];
|
||||
const simulationArgs = ['-id', confID, '-test'];
|
||||
|
||||
// 调用后端API执行测试
|
||||
const response = await fetch('/api/run-simulation', {
|
||||
@ -207,8 +183,12 @@ class RunTest extends HTMLElement {
|
||||
|
||||
const responseData = await response.json();
|
||||
|
||||
// 连接到SSE获取实时输出
|
||||
this.connectToEventSource(responseData.simulationId);
|
||||
// 获取进程ID和日志文件路径
|
||||
const processId = responseData.simulationId;
|
||||
const logFile = responseData.logFile;
|
||||
|
||||
// 开始轮询日志文件
|
||||
this.startLogFilePolling(logFile);
|
||||
|
||||
// 根据测试结果更新UI
|
||||
if (responseData.success) {
|
||||
@ -232,92 +212,79 @@ class RunTest extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// 连接到SSE事件源获取实时输出
|
||||
connectToEventSource(simulationId) {
|
||||
// 关闭之前的连接
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
// 开始轮询日志文件
|
||||
startLogFilePolling(logFile) {
|
||||
// 清除之前的轮询
|
||||
if (this.logFilePollingInterval) {
|
||||
clearInterval(this.logFilePollingInterval);
|
||||
this.logFilePollingInterval = null;
|
||||
}
|
||||
|
||||
// 创建新的SSE连接
|
||||
const url = `/api/simulation-output/${simulationId}`;
|
||||
this.eventSource = new EventSource(url);
|
||||
|
||||
// 标准输出和错误输出
|
||||
this.eventSource.addEventListener('output', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
let lastPosition = 0;
|
||||
const outputContent = this.shadowRoot.querySelector('#output-content');
|
||||
|
||||
// 添加新输出并应用ANSI颜色
|
||||
if (data.data) {
|
||||
const html = this.convertAnsiToHtml(data.data);
|
||||
outputContent.innerHTML += html;
|
||||
// 每100ms检查一次文件
|
||||
this.logFilePollingInterval = setInterval(async () => {
|
||||
try {
|
||||
// 检查进程是否还在运行
|
||||
const response = await fetch(`/api/check-process/${logFile.split('_')[1].split('.')[0]}`);
|
||||
const data = await response.json();
|
||||
|
||||
// 自动滚动到底部
|
||||
if (!data.running) {
|
||||
// 进程已结束,读取剩余内容并停止轮询
|
||||
const finalContent = await this.readLogFile(logFile, lastPosition);
|
||||
if (finalContent) {
|
||||
outputContent.innerHTML += this.convertAnsiToHtml(finalContent);
|
||||
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 || '测试已结束');
|
||||
this.closeEventSource();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理状态事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 仿真完成
|
||||
this.eventSource.addEventListener('completed', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
clearInterval(this.logFilePollingInterval);
|
||||
this.logFilePollingInterval = null;
|
||||
|
||||
if (data.success) {
|
||||
this.showSuccess('测试执行成功');
|
||||
} else {
|
||||
this.showError(`测试执行失败: ${data.message}`);
|
||||
this.showError(`测试执行失败: ${data.message || '未知错误'}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeEventSource();
|
||||
// 读取新的日志内容
|
||||
const content = await this.readLogFile(logFile, lastPosition);
|
||||
if (content) {
|
||||
outputContent.innerHTML += this.convertAnsiToHtml(content);
|
||||
outputContent.scrollTop = outputContent.scrollHeight;
|
||||
lastPosition += content.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理完成事件失败:', error);
|
||||
console.error('读取日志文件失败:', error);
|
||||
clearInterval(this.logFilePollingInterval);
|
||||
this.logFilePollingInterval = null;
|
||||
this.showError('读取日志文件失败: ' + error.message);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 仿真错误
|
||||
this.eventSource.addEventListener('error', (event) => {
|
||||
// 读取日志文件
|
||||
async readLogFile(logFile, startPosition) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.showError(`测试错误: ${data.message}`);
|
||||
this.closeEventSource();
|
||||
const response = await fetch(`/api/read-log-file?file=${encodeURIComponent(logFile)}&position=${startPosition}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('读取日志文件失败');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.content;
|
||||
} catch (error) {
|
||||
console.error('处理错误事件失败:', error);
|
||||
console.error('读取日志文件失败:', error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 连接错误处理
|
||||
this.eventSource.onerror = (error) => {
|
||||
console.error('SSE连接错误:', error);
|
||||
this.showError('实时输出连接已断开');
|
||||
this.closeEventSource();
|
||||
};
|
||||
}
|
||||
|
||||
// 关闭SSE连接
|
||||
closeEventSource() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
// 在组件销毁时清理资源
|
||||
disconnectedCallback() {
|
||||
if (this.logFilePollingInterval) {
|
||||
clearInterval(this.logFilePollingInterval);
|
||||
this.logFilePollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -361,52 +328,6 @@ class RunTest extends HTMLElement {
|
||||
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 {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -415,7 +336,7 @@ class RunTest extends HTMLElement {
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
width: 250px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
@ -450,9 +371,14 @@ class RunTest extends HTMLElement {
|
||||
.list-group-header {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.list-group-info {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.list-items {
|
||||
@ -477,6 +403,15 @@ class RunTest extends HTMLElement {
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
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 {
|
||||
@ -490,47 +425,60 @@ class RunTest extends HTMLElement {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 终端颜色支持 */
|
||||
.output-content .ansi-black { color: #000000; }
|
||||
.output-content .ansi-red { color: #ff0000; }
|
||||
.output-content .ansi-green { color: #00ff00; }
|
||||
.output-content .ansi-yellow { color: #ffff00; }
|
||||
.output-content .ansi-blue { color: #0000ff; }
|
||||
.output-content .ansi-magenta { color: #ff00ff; }
|
||||
.output-content .ansi-cyan { color: #00ffff; }
|
||||
.output-content .ansi-white { color: #ffffff; }
|
||||
/* ANSI颜色样式 */
|
||||
.ansi-black { color: #000000; }
|
||||
.ansi-red { color: #ff0000; }
|
||||
.ansi-green { color: #00ff00; }
|
||||
.ansi-yellow { color: #ffff00; }
|
||||
.ansi-blue { color: #0000ff; }
|
||||
.ansi-magenta { color: #ff00ff; }
|
||||
.ansi-cyan { color: #00ffff; }
|
||||
.ansi-white { color: #ffffff; }
|
||||
|
||||
.output-content .ansi-bright-black { color: #808080; }
|
||||
.output-content .ansi-bright-red { color: #ff5555; }
|
||||
.output-content .ansi-bright-green { color: #55ff55; }
|
||||
.output-content .ansi-bright-yellow { color: #ffff55; }
|
||||
.output-content .ansi-bright-blue { color: #5555ff; }
|
||||
.output-content .ansi-bright-magenta { color: #ff55ff; }
|
||||
.output-content .ansi-bright-cyan { color: #55ffff; }
|
||||
.output-content .ansi-bright-white { color: #ffffff; }
|
||||
.ansi-bright-black { color: #666666; }
|
||||
.ansi-bright-red { color: #ff6666; }
|
||||
.ansi-bright-green { color: #66ff66; }
|
||||
.ansi-bright-yellow { color: #ffff66; }
|
||||
.ansi-bright-blue { color: #6666ff; }
|
||||
.ansi-bright-magenta { color: #ff66ff; }
|
||||
.ansi-bright-cyan { color: #66ffff; }
|
||||
.ansi-bright-white { color: #ffffff; }
|
||||
|
||||
.output-content .ansi-bg-black { background-color: #000000; }
|
||||
.output-content .ansi-bg-red { background-color: #ff0000; }
|
||||
.output-content .ansi-bg-green { background-color: #00ff00; }
|
||||
.output-content .ansi-bg-yellow { background-color: #ffff00; }
|
||||
.output-content .ansi-bg-blue { background-color: #0000ff; }
|
||||
.output-content .ansi-bg-magenta { background-color: #ff00ff; }
|
||||
.output-content .ansi-bg-cyan { background-color: #00ffff; }
|
||||
.output-content .ansi-bg-white { background-color: #ffffff; }
|
||||
.ansi-bg-black { background-color: #000000; }
|
||||
.ansi-bg-red { background-color: #ff0000; }
|
||||
.ansi-bg-green { background-color: #00ff00; }
|
||||
.ansi-bg-yellow { background-color: #ffff00; }
|
||||
.ansi-bg-blue { background-color: #0000ff; }
|
||||
.ansi-bg-magenta { background-color: #ff00ff; }
|
||||
.ansi-bg-cyan { background-color: #00ffff; }
|
||||
.ansi-bg-white { background-color: #ffffff; }
|
||||
|
||||
.output-content .ansi-bold { font-weight: bold; }
|
||||
.output-content .ansi-italic { font-style: italic; }
|
||||
.output-content .ansi-underline { text-decoration: underline; }
|
||||
.ansi-bold { font-weight: bold; }
|
||||
.ansi-italic { font-style: italic; }
|
||||
.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>
|
||||
<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="lists-container">
|
||||
<div class="list-section">
|
||||
@ -543,7 +491,11 @@ class RunTest extends HTMLElement {
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
@ -553,9 +505,6 @@ class RunTest extends HTMLElement {
|
||||
// 添加事件监听器
|
||||
const runButton = this.shadowRoot.querySelector('#run-button');
|
||||
runButton.addEventListener('click', () => this.runTest());
|
||||
|
||||
const scenarioSelect = this.shadowRoot.querySelector('#scenario-select');
|
||||
scenarioSelect.addEventListener('change', (event) => this.onScenarioSelected(event));
|
||||
}
|
||||
|
||||
// ANSI终端颜色转换为HTML
|
||||
|
@ -2,7 +2,8 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { spawn, exec } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const fs = require('fs');
|
||||
const fsPromises = require('fs').promises;
|
||||
const util = require('util');
|
||||
const execPromise = util.promisify(exec);
|
||||
const {
|
||||
@ -237,7 +238,7 @@ router.post('/run-simulation', async (req, res) => {
|
||||
if (!existingProcess) {
|
||||
// 创建日志文件
|
||||
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`);
|
||||
|
||||
// 从命令行参数中提取配置文件路径
|
||||
@ -288,7 +289,7 @@ router.post('/run-simulation', async (req, res) => {
|
||||
|
||||
// 检查引擎程序是否存在
|
||||
try {
|
||||
await fs.access(enginePath);
|
||||
await fsPromises.access(enginePath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({
|
||||
error: 'XNEngine不存在',
|
||||
@ -298,7 +299,7 @@ router.post('/run-simulation', async (req, res) => {
|
||||
|
||||
// 创建日志文件
|
||||
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`);
|
||||
|
||||
// 使用nohup启动进程,将输出重定向到日志文件
|
||||
@ -504,4 +505,86 @@ router.post('/cleanup-simulation/:id', (req, res) => {
|
||||
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;
|
Loading…
x
Reference in New Issue
Block a user