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;