更新了运行测试页面
This commit is contained in:
parent
7c5021958d
commit
5ae0ecae52
Binary file not shown.
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user