feat(steady): 实现稳态校验任务功能重构
- 添加influxdb配置支持和资源文件打包 - 实现校验任务表格组件和相关工具函数 - 重构校验工作台为任务创建对话框模式 - 实现校验详情面板支持多种异常类型展示 - 更新校验概览表格显示任务基本信息 - 优化校验查询参数和API接口定义 - 实现搜索表单组件化和过滤功能增强
This commit is contained in:
410
scripts/influxdb-process-manager.js
Normal file
410
scripts/influxdb-process-manager.js
Normal 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;
|
||||
Reference in New Issue
Block a user