Files
CN_Tool_client/scripts/influxdb-process-manager.js
yexb 8622f25048 feat(steady): 实现稳态校验任务功能重构
- 添加influxdb配置支持和资源文件打包
- 实现校验任务表格组件和相关工具函数
- 重构校验工作台为任务创建对话框模式
- 实现校验详情面板支持多种异常类型展示
- 更新校验概览表格显示任务基本信息
- 优化校验查询参数和API接口定义
- 实现搜索表单组件化和过滤功能增强
2026-06-11 10:53:02 +08:00

411 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;