feat(steady): 实现稳态校验任务功能重构

- 添加influxdb配置支持和资源文件打包
- 实现校验任务表格组件和相关工具函数
- 重构校验工作台为任务创建对话框模式
- 实现校验详情面板支持多种异常类型展示
- 更新校验概览表格显示任务基本信息
- 优化校验查询参数和API接口定义
- 实现搜索表单组件化和过滤功能增强
This commit is contained in:
2026-06-11 10:53:02 +08:00
parent 3dff953b8d
commit 8622f25048
25 changed files with 1675 additions and 486 deletions

View File

@@ -0,0 +1,410 @@
const { spawn, execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
const { resolvePackagedRuntime, resolveRuntimeStrategy } = require('./path-utils');
/**
* InfluxDB 进程管理器。
* 参照 MySQL 绿色包模式,随应用启动本地 InfluxDB退出时只清理本应用记录的进程。
*/
class InfluxDBProcessManager {
constructor(logWindowManager = null) {
const runtime = resolvePackagedRuntime();
const baseDir = runtime.baseDir;
const sourceInfluxdbPath = !runtime.isPackaged
? path.join(baseDir, 'build', 'extraResources', 'influxdb-1.7.0')
: path.join(baseDir, 'influxdb-1.7.0');
const pathStrategy = resolveRuntimeStrategy(baseDir);
this.baseDir = baseDir;
this.pathStrategy = pathStrategy;
this.sourceInfluxdbPath = sourceInfluxdbPath;
this.influxdbPath = pathStrategy.usesSafePaths
? path.join(pathStrategy.safeRuntimeRoot, 'influxdb-1.7.0')
: sourceInfluxdbPath;
this.dataPath = path.join(this.influxdbPath, 'data');
this.metaPath = path.join(this.influxdbPath, 'meta');
this.walPath = path.join(this.influxdbPath, 'wal');
this.configFile = path.join(this.influxdbPath, 'influxdb.runtime.conf');
this.processRecordFile = path.join(this.influxdbPath, '.running-process.json');
this.logWindowManager = logWindowManager;
this.influxdbProcess = null;
this.currentPort = null;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
log(type, message) {
console.log(`[InfluxDB] ${message}`);
if (this.logWindowManager) {
this.logWindowManager.addLog(type, `[InfluxDB] ${message}`);
}
}
normalizeComparablePath(target = '') {
return String(target).replace(/"/g, '').replace(/\//g, '\\').toLowerCase();
}
copyDirectorySync(sourceDir, targetDir) {
if (!fs.existsSync(sourceDir)) {
return;
}
fs.mkdirSync(targetDir, { recursive: true });
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
entries.forEach((entry) => {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
if (entry.isDirectory()) {
this.copyDirectorySync(sourcePath, targetPath);
return;
}
let shouldCopy = !fs.existsSync(targetPath);
if (!shouldCopy) {
const sourceStat = fs.statSync(sourcePath);
const targetStat = fs.statSync(targetPath);
shouldCopy = sourceStat.size !== targetStat.size || sourceStat.mtimeMs > targetStat.mtimeMs;
}
if (shouldCopy) {
fs.copyFileSync(sourcePath, targetPath);
}
});
}
ensureRuntimeEnvironment() {
if (!fs.existsSync(this.sourceInfluxdbPath)) {
throw new Error(`InfluxDB 目录不存在: ${this.sourceInfluxdbPath}`);
}
if (this.pathStrategy.usesSafePaths) {
this.log('system', '检测到应用路径包含非 ASCII 字符,启用 InfluxDB 英文安全运行目录');
this.log('system', `InfluxDB 源目录: ${this.sourceInfluxdbPath}`);
this.log('system', `InfluxDB 运行目录: ${this.influxdbPath}`);
this.copyDirectorySync(this.sourceInfluxdbPath, this.influxdbPath);
}
fs.mkdirSync(this.dataPath, { recursive: true });
fs.mkdirSync(this.metaPath, { recursive: true });
fs.mkdirSync(this.walPath, { recursive: true });
}
saveProcessRecord(record = {}) {
try {
fs.mkdirSync(path.dirname(this.processRecordFile), { recursive: true });
fs.writeFileSync(this.processRecordFile, JSON.stringify(record, null, 2), 'utf-8');
} catch (error) {
this.log('warn', `写入 InfluxDB 进程标记失败: ${error.message}`);
}
}
getProcessRecord() {
try {
if (!fs.existsSync(this.processRecordFile)) {
return null;
}
return JSON.parse(fs.readFileSync(this.processRecordFile, 'utf-8'));
} catch (error) {
this.log('warn', `读取 InfluxDB 进程标记失败: ${error.message}`);
return null;
}
}
clearProcessRecord() {
try {
if (fs.existsSync(this.processRecordFile)) {
fs.unlinkSync(this.processRecordFile);
}
} catch (error) {
this.log('warn', `清理 InfluxDB 进程标记失败: ${error.message}`);
}
}
execFileCommand(command, args = []) {
return new Promise((resolve, reject) => {
execFile(command, args, { encoding: 'utf8', windowsHide: true }, (error, stdout, stderr) => {
if (error) {
const err = new Error(error.message || stderr || stdout || 'Command execution failed');
err.code = error.code;
err.stdout = stdout;
err.stderr = stderr;
reject(err);
} else {
resolve({ stdout, stderr });
}
});
});
}
async getProcessInfoByPid(pid) {
if (!pid) {
return null;
}
try {
const script = `$proc = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($proc) { [PSCustomObject]@{ executablePath = $proc.ExecutablePath; commandLine = $proc.CommandLine; name = $proc.Name } | ConvertTo-Json -Compress }`;
const { stdout } = await this.execFileCommand('powershell.exe', ['-NoProfile', '-Command', script]);
const raw = (stdout || '').trim();
return raw ? JSON.parse(raw) : null;
} catch (error) {
return null;
}
}
async isOwnInfluxDBProcess(pid, record = this.getProcessRecord()) {
const processInfo = await this.getProcessInfoByPid(pid);
if (!processInfo) {
return false;
}
const expectedExe = this.normalizeComparablePath(path.join(this.influxdbPath, 'influxd.exe'));
const expectedBat = this.normalizeComparablePath(path.join(this.influxdbPath, 'start-influxdb.bat'));
const expectedConfig = this.normalizeComparablePath((record && record.configFile) || this.configFile);
const executablePath = this.normalizeComparablePath(processInfo.executablePath);
const commandLine = this.normalizeComparablePath(processInfo.commandLine);
return (
(executablePath === expectedExe || commandLine.includes(expectedBat)) &&
(!expectedConfig || commandLine.includes(expectedConfig))
);
}
async waitForProcessExit(pid, timeoutMs = 3000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const processInfo = await this.getProcessInfoByPid(pid);
if (!processInfo) {
return true;
}
await this.sleep(200);
}
return false;
}
async terminateTrackedProcess(record = this.getProcessRecord(), reason = '正在停止 InfluxDB 进程...') {
const pid = this.influxdbProcess?.pid || record?.pid;
const port = this.currentPort || record?.port;
if (!pid) {
this.log('system', '未找到可清理的 InfluxDB 进程标记');
this.clearProcessRecord();
return false;
}
const isOwnProcess = await this.isOwnInfluxDBProcess(pid, record);
if (!isOwnProcess) {
this.log('warn', `PID ${pid} 不是当前应用的 InfluxDB 进程,跳过清理`);
this.clearProcessRecord();
if (this.influxdbProcess && this.influxdbProcess.pid === pid) {
this.influxdbProcess = null;
}
return false;
}
this.log('system', `${reason} PID=${pid}${port ? `, 端口=${port}` : ''}`);
try {
await this.execFileCommand('taskkill.exe', ['/F', '/T', '/PID', String(pid)]);
await this.waitForProcessExit(pid, 3000);
this.log('success', 'InfluxDB 进程已停止');
} finally {
this.clearProcessRecord();
if (this.influxdbProcess && this.influxdbProcess.pid === pid) {
this.influxdbProcess = null;
}
this.currentPort = null;
}
return true;
}
async checkAndKillOrphanProcess() {
const record = this.getProcessRecord();
if (!record || !record.pid) {
return;
}
const isOwnProcess = await this.isOwnInfluxDBProcess(record.pid, record);
if (!isOwnProcess) {
this.log('warn', `检测到失效的 InfluxDB 进程标记 PID=${record.pid},已跳过清理并移除标记`);
this.clearProcessRecord();
return;
}
this.log('warn', `检测到当前应用上次遗留的 InfluxDB 进程 PID=${record.pid},开始定向清理...`);
await this.terminateTrackedProcess(record, '正在清理当前应用遗留的 InfluxDB 进程...');
}
generateConfig(port) {
const normalizePath = target => target.replace(/\\/g, '/');
const config = `reporting-disabled = true
bind-address = "127.0.0.1:8088"
[meta]
dir = "${normalizePath(this.metaPath)}"
[data]
dir = "${normalizePath(this.dataPath)}"
wal-dir = "${normalizePath(this.walPath)}"
[http]
enabled = true
bind-address = "127.0.0.1:${port}"
auth-enabled = false
log-enabled = true
`;
fs.writeFileSync(this.configFile, config, 'utf-8');
this.log('system', `生成 InfluxDB 配置文件,端口: ${port}`);
}
async startInfluxDBProcess(port) {
return new Promise((resolve, reject) => {
const influxd = path.join(this.influxdbPath, 'influxd.exe');
const startBat = path.join(this.influxdbPath, 'start-influxdb.bat');
this.log('system', '正在启动 InfluxDB 进程...');
this.log('system', `可执行文件: ${influxd}`);
this.log('system', `启动脚本: ${startBat}`);
this.log('system', `配置文件: ${this.configFile}`);
if (!fs.existsSync(influxd)) {
return reject(new Error(`influxd.exe 不存在: ${influxd}`));
}
if (!fs.existsSync(startBat)) {
return reject(new Error(`start-influxdb.bat 不存在: ${startBat}`));
}
if (!fs.existsSync(this.configFile)) {
return reject(new Error(`配置文件不存在: ${this.configFile}`));
}
const influxdbProcess = spawn('cmd.exe', ['/d', '/s', '/c', 'call', startBat, this.configFile], {
cwd: this.influxdbPath,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
});
this.influxdbProcess = influxdbProcess;
this.currentPort = port;
this.saveProcessRecord({
pid: influxdbProcess.pid,
port,
executablePath: influxd,
configFile: this.configFile,
influxdbPath: this.influxdbPath,
createdAt: new Date().toISOString()
});
let startupComplete = false;
let startupTimeout = null;
const completeStartup = () => {
if (!startupComplete) {
startupComplete = true;
if (startupTimeout) clearTimeout(startupTimeout);
this.log('success', 'InfluxDB 进程已就绪');
resolve(true);
}
};
const handleOutput = (data) => {
const msg = data.toString().trim();
if (!msg) {
return;
}
if (msg.includes('Listening on HTTP') || msg.includes('Listening for signals')) {
completeStartup();
}
if (!msg.includes('[I]') || msg.includes('Listening') || msg.includes('error')) {
this.log('system', `[influxd] ${msg}`);
}
};
influxdbProcess.stdout.on('data', handleOutput);
influxdbProcess.stderr.on('data', handleOutput);
influxdbProcess.on('error', (err) => {
this.log('error', `InfluxDB 进程启动失败: ${err.message}`);
this.clearProcessRecord();
this.influxdbProcess = null;
reject(err);
});
influxdbProcess.on('exit', (code, signal) => {
this.log('system', `InfluxDB 进程退出,代码: ${code}, 信号: ${signal}`);
const record = this.getProcessRecord();
if (record && record.pid === influxdbProcess.pid) {
this.clearProcessRecord();
}
this.influxdbProcess = null;
this.currentPort = null;
if (!startupComplete) {
reject(new Error(`InfluxDB 进程异常退出,代码: ${code}`));
}
});
startupTimeout = setTimeout(completeStartup, 15000);
});
}
async stopInfluxDBProcess() {
const record = this.getProcessRecord();
if (!this.influxdbProcess && !record) {
this.log('system', 'InfluxDB 进程未运行');
return;
}
await this.terminateTrackedProcess(record, '正在停止当前应用自己的 InfluxDB 进程...');
}
isInfluxDBRunning() {
return this.influxdbProcess !== null && !this.influxdbProcess.killed;
}
async ensureServiceRunning(findAvailablePort, waitForPort) {
this.ensureRuntimeEnvironment();
this.log('system', '开始 InfluxDB 进程检查流程(进程模式)');
this.log('system', `路径策略: ${this.pathStrategy.usesSafePaths ? '英文安全路径模式' : '应用目录直启模式'}`);
this.log('system', `InfluxDB 路径: ${this.influxdbPath}`);
this.log('system', `配置文件: ${this.configFile}`);
if (this.isInfluxDBRunning()) {
const port = this.currentPort || 18086;
this.log('success', `InfluxDB 进程已在运行,端口: ${port}`);
return port;
}
await this.checkAndKillOrphanProcess();
const port = await findAvailablePort(18086, 100);
if (port === -1) {
throw new Error('无法找到可用的 InfluxDB 端口18086-18185 全部被占用)');
}
this.log('system', `找到可用端口: ${port}`);
this.generateConfig(port);
await this.startInfluxDBProcess(port);
this.log('system', `等待端口 ${port} 就绪...`);
const portReady = await waitForPort(port, 30000);
if (!portReady) {
throw new Error(`InfluxDB 端口 ${port} 未能在 30 秒内就绪`);
}
this.log('success', `InfluxDB 进程启动成功,端口: ${port}`);
return port;
}
}
module.exports = InfluxDBProcessManager;