"use strict"; /** * 巡检模块统一入口 * * 提供 IPC handler 注册函数,供主进程注册。 * 渲染进程通过 IPC 调用巡检相关功能。 */ const deviceStore = require("./deviceStore.cjs"); const alertRuleStore = require("./alertRuleStore.cjs"); const inspectionResultStore = require("./inspectionResultStore.cjs"); const alertRecordStore = require("./alertRecordStore.cjs"); const adapterRegistry = require("./adapterRegistry.cjs"); const InspectionScheduler = require("./inspectionScheduler.cjs"); const { execSshCommand } = require("./inspectionExec.cjs"); const { executeBoundedSshCommand } = require("../boundedSshExec.cjs"); const { ALERT_LEVEL_LABELS, DEVICE_TIER_LABELS, INSPECTION_STATUS_LABELS, ALERT_RECORD_STATUS_LABELS, OPERATOR_LABELS, DEVICE_CATALOG_LABELS, METRIC_LABELS, INSPECTION_STATUS, } = require("./types.cjs"); let _scheduler = null; /** * 获取标签映射(前端展示用) */ function getLabels() { return { alert_levels: ALERT_LEVEL_LABELS, device_tiers: DEVICE_TIER_LABELS, inspection_status: INSPECTION_STATUS_LABELS, alert_record_status: ALERT_RECORD_STATUS_LABELS, operators: OPERATOR_LABELS, device_catalog: DEVICE_CATALOG_LABELS, metrics: METRIC_LABELS, }; } /** * 注册巡检模块的 IPC handlers * * @param {object} ipcMain - Electron ipcMain * @param {object} options * @param {function} options.readStorage - 读取本地存储 (key) => string * @param {function} options.writeStorage - 写入本地存储 (key, value) => void */ function registerInspectionHandlers(ipcMain, options) { const { readStorage, writeStorage, sessions } = options; const readFn = (key) => readStorage(key); const writeFn = (key, val) => writeStorage(key, val); // 根据主机地址查找会话(用于复用终端 SSH 连接) function findSessionByHost(hostname) { const DEBUG = process.env.INSPECTION_DEBUG === "1"; if (DEBUG) console.log("[Inspection] 查找会话,总会话数:", sessions?.size || 0); if (!sessions) { if (DEBUG) console.log("[Inspection] sessions 未初始化"); return null; } for (const session of sessions.values()) { if (!session) continue; // SSH 会话才有 conn if (!session.conn) continue; // 匹配主机地址 const sessionHost = session.hostname || session.host || ""; if (DEBUG) { console.log("[Inspection] SSH会话:", sessionHost, "| 目标:", hostname); } if (sessionHost === hostname) { if (DEBUG) console.log("[Inspection] 匹配到会话:", sessionHost); return session; } } return null; } // ---------- 标签映射 ---------- ipcMain.handle("inspection:getLabels", () => { return getLabels(); }); // ---------- 系统配置 ---------- ipcMain.handle("inspection:getSysConfig", () => { return deviceStore.getSysConfig(readFn); }); ipcMain.handle("inspection:updateSysConfig", (_e, updates) => { return deviceStore.updateSysConfig(readFn, writeFn, updates); }); // ---------- 设备类型 ---------- ipcMain.handle("inspection:getCatalog", () => { return deviceStore.getCatalog(); }); ipcMain.handle("inspection:getTiers", () => { return deviceStore.getTiers(); }); ipcMain.handle("inspection:getAdapters", () => { return adapterRegistry.getRegisteredAdapters(); }); // ---------- 设备扩展配置 ---------- ipcMain.handle("inspection:getAllExtensions", () => { return deviceStore.getAllExtensions(readFn); }); ipcMain.handle("inspection:getExtension", (_e, hostId) => { return deviceStore.getExtension(readFn, hostId); }); ipcMain.handle("inspection:upsertExtension", (_e, hostId, data) => { return deviceStore.upsertExtension(readFn, writeFn, hostId, data); }); ipcMain.handle("inspection:deleteExtension", (_e, hostId) => { return deviceStore.deleteExtension(readFn, writeFn, hostId); }); ipcMain.handle("inspection:getEffectiveInterval", (_e, hostId) => { return deviceStore.getEffectiveInterval(readFn, hostId); }); ipcMain.handle("inspection:getEnabledDevices", () => { return deviceStore.getEnabledDevices(readFn); }); // ---------- 告警规则 ---------- ipcMain.handle("inspection:listRules", () => { return alertRuleStore.listRules(readFn); }); ipcMain.handle("inspection:getRule", (_e, ruleId) => { return alertRuleStore.getRule(readFn, ruleId); }); ipcMain.handle("inspection:createRule", (_e, data) => { return alertRuleStore.createRule(readFn, writeFn, data); }); ipcMain.handle("inspection:updateRule", (_e, ruleId, updates) => { return alertRuleStore.updateRule(readFn, writeFn, ruleId, updates); }); ipcMain.handle("inspection:deleteRule", (_e, ruleId) => { return alertRuleStore.deleteRule(readFn, writeFn, ruleId); }); ipcMain.handle("inspection:toggleRule", (_e, ruleId, enabled) => { return alertRuleStore.toggleRule(readFn, writeFn, ruleId, enabled); }); // ---------- 巡检结果 ---------- ipcMain.handle("inspection:getLatestAll", () => { return inspectionResultStore.getLatestAll(readFn); }); ipcMain.handle("inspection:getLatestByDevice", (_e, hostId, limit) => { return inspectionResultStore.getLatestByDevice(readFn, hostId, limit); }); ipcMain.handle("inspection:getRecentResults", (_e, limit) => { return inspectionResultStore.getRecent(readFn, limit); }); // ---------- 单次巡检执行(前端传入完整主机配置) ---------- ipcMain.handle("inspection:runInspection", async (_e, payload) => { const { hostId, hostConfig, catalogId } = payload || {}; if (!hostId || !hostConfig) { return { success: false, error: "缺少主机配置" }; } const DEBUG = process.env.INSPECTION_DEBUG === "1"; if (DEBUG) { console.log("[Inspection] 巡检配置:", JSON.stringify({ host: hostConfig.hostname || hostConfig.address, port: hostConfig.port || 22, username: hostConfig.username, hasPassword: !!hostConfig.password, hasPrivateKey: !!hostConfig.privateKeyContent, hasKeyId: !!hostConfig.identityFileId, })); } const sshConfig = { host: hostConfig.hostname || hostConfig.address, port: hostConfig.port || 22, username: hostConfig.username, password: hostConfig.password, privateKey: hostConfig.privateKeyContent, passphrase: hostConfig.passphrase, }; if (!sshConfig.host || !sshConfig.username) { return { success: false, error: "主机地址或用户名缺失" }; } const startTime = Date.now(); const result = { id: `ins_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, host_id: hostId, catalog_id: catalogId || "linux_server", start_time: startTime, end_time: 0, status: INSPECTION_STATUS.SUCCESS, metrics: {}, raw: {}, error: null, alerts: [], }; try { // 构造 execFn — 优先复用终端会话的 SSH 连接 const execFn = async (command, options = {}) => { const timeoutMs = options.timeoutMs || 60000; const DEBUG = process.env.INSPECTION_DEBUG === "1"; // 1. 先尝试复用终端会话的连接 const session = findSessionByHost(sshConfig.host); if (DEBUG) { console.log("[Inspection] 查找会话:", sshConfig.host, "| 找到会话:", session ? "是" : "否", "| 有连接:", session?.conn ? "是" : "否"); } if (session && session.conn) { try { const result = await executeBoundedSshCommand(session.conn, command, { openingTimeoutMs: timeoutMs, runTimeoutMs: timeoutMs, maxOutputBytes: 100 * 1024, }); if (DEBUG) console.log("[Inspection] 复用会话成功, stdout:", result.stdout?.slice(0, 100)); return result.stdout + (result.stderr ? "\n" + result.stderr : ""); } catch (e) { // 终端连接失败,回退到新建连接 if (DEBUG) console.log("[Inspection] 复用会话失败,回退:", e.message); } } // 2. 回退:新建 SSH 连接 return await execSshCommand(sshConfig, command, { connectTimeoutMs: 10000, execTimeoutMs: timeoutMs, }); }; // 构造批量执行函数(一次连接执行多条命令) const execCommandsFn = async (commands, options = {}) => { const { execSshCommands } = require("./inspectionExec.cjs"); return await execSshCommands(sshConfig, commands, options); }; // 获取设备类型对应的采集器类名 const catalogs = deviceStore.getCatalog(); const catalog = catalogs.find((c) => c.id === catalogId); const adapterClassName = catalog?.adapter_class || "LinuxServerAdapter"; // 创建采集器实例(注入 execFn 和 execCommandsFn) const adapter = adapterRegistry.createAdapter(adapterClassName, sshConfig, { execFn, execCommands: execCommandsFn, timeoutMs: 20000, }); if (!adapter) { throw new Error("采集器加载失败: " + adapterClassName); } const collectResult = await adapter.collect(); result.metrics = collectResult.metrics || {}; result.raw = collectResult.raw || {}; if (!collectResult.online) { result.status = collectResult.status || INSPECTION_STATUS.FAILED; result.error = collectResult.error || "设备离线或连接失败"; } } catch (err) { result.status = INSPECTION_STATUS.FAILED; result.error = err.message; console.error("[Inspection] runInspection error:", err.message); } result.end_time = Date.now(); const duration = result.end_time - result.start_time; // 构造设备信息(用于告警记录) const deviceInfo = { device_id: hostId, device_name: hostConfig.label || hostConfig.hostname || hostId, catalog_id: catalogId || "linux_server", }; // 保存巡检结果 try { inspectionResultStore.addResult(readFn, writeFn, { ...result, device_id: hostId, online: result.status === INSPECTION_STATUS.SUCCESS, duration_ms: duration, error_message: result.error, }); } catch (e) { console.warn("[Inspection] 保存巡检结果失败:", e.message); } // 告警判断(成功/失败都要判断,特别是 online 指标) const triggeredAlerts = []; try { const deviceExt = deviceStore.getExtension(readFn, hostId); const rules = alertRuleStore.getRulesForDevice(readFn, deviceExt || { host_id: hostId, catalog_id: catalogId }); const isOnline = result.status === INSPECTION_STATUS.SUCCESS; for (const rule of rules) { // online 指标 — 成功/失败都要判断(设备离线告警) if (rule.metric === "online") { const shouldAlert = rule.threshold === "false" ? !isOnline : isOnline; console.log(`[Inspection] 离线规则: name=${rule.name} enabled=${rule.enabled} isOnline=${isOnline} shouldAlert=${shouldAlert}`); const alertResult = alertRecordStore.processAlert( readFn, writeFn, deviceInfo, rule, isOnline ? "在线" : "离线", shouldAlert ); if (alertResult) { console.log(`[Inspection] 告警触发:`, JSON.stringify(alertResult)); triggeredAlerts.push(alertResult); } continue; } // 普通指标 — 只在成功时判断(失败时没有指标数据) if (isOnline) { const metricValue = result.metrics[rule.metric]; if (metricValue === undefined || metricValue === null) continue; const isTriggered = alertRuleStore.evaluateRule(metricValue, rule.operator, rule.threshold); const alertResult = alertRecordStore.processAlert( readFn, writeFn, deviceInfo, rule, metricValue, isTriggered ); if (alertResult) { triggeredAlerts.push(alertResult); } } } } catch (e) { console.warn("[Inspection] 告警判断失败:", e.message); } return { success: result.status === INSPECTION_STATUS.SUCCESS, status: result.status, metrics: result.metrics, error: result.error, duration, alerts: triggeredAlerts, }; }); // ---------- 告警记录 ---------- ipcMain.handle("inspection:listAlerts", (_e, options) => { return alertRecordStore.listAlerts(readFn, options || {}); }); ipcMain.handle("inspection:getActiveAlerts", () => { return alertRecordStore.getActiveAlerts(readFn); }); ipcMain.handle("inspection:getAlertStats", () => { return alertRecordStore.getAlertStats(readFn); }); ipcMain.handle("inspection:acknowledgeAlert", (_e, alertId, operator) => { return alertRecordStore.acknowledgeAlert(readFn, writeFn, alertId, operator); }); ipcMain.handle("inspection:resolveAlert", (_e, alertId, reason, operator) => { return alertRecordStore.resolveAlert(readFn, writeFn, alertId, reason, operator); }); ipcMain.handle("inspection:closeAlert", (_e, alertId, note) => { return alertRecordStore.closeAlert(readFn, writeFn, alertId, note); }); ipcMain.handle("inspection:updateAlertNote", (_e, alertId, note) => { return alertRecordStore.updateNote(readFn, writeFn, alertId, note); }); // 存储前端传入的设备 SSH 配置(hostId → sshConfig) let _hostConfigs = null; // ---------- 调度器控制 ---------- ipcMain.handle("inspection:scheduler:start", (_e, options = {}) => { // 前端可传入 { hostConfigs: { [hostId]: sshConfig } } 用于调度器执行 SSH 命令 if (options.hostConfigs) { _hostConfigs = options.hostConfigs; } // 从 hostConfigs 提取 hostInfo(hostname/label)用于告警显示 const hostInfo = {}; if (_hostConfigs) { for (const [hostId, config] of Object.entries(_hostConfigs)) { hostInfo[hostId] = { hostname: config.host || config.hostname || "", label: config.label || "", }; } } if (!_scheduler) { _scheduler = new InspectionScheduler({ readFn, writeFn, execFn: async (hostId, command) => { // 优先用前端传入的 hostConfigs if (_hostConfigs && _hostConfigs[hostId]) { const config = _hostConfigs[hostId]; const { execSshCommand: execCmd } = require("./inspectionExec.cjs"); return await execCmd(config, command, { connectTimeoutMs: 10000, execTimeoutMs: 30000, }); } // 兜底:尝试复用已有终端会话 const session = findSessionByHost(hostId); if (session?.conn) { const { executeBoundedSshCommand } = require("../boundedSshExec.cjs"); return await executeBoundedSshCommand(session.conn, command, { timeoutMs: 30000, }); } throw new Error(`无法执行巡检:未找到主机 ${hostId} 的 SSH 配置`); }, pollIntervalMs: 30 * 1000, concurrency: 5, timeoutMs: 30 * 1000, hostInfo, }); } _scheduler.start(); return _scheduler.getStatus(); }); ipcMain.handle("inspection:scheduler:stop", () => { if (_scheduler) { _scheduler.stop(); return _scheduler.getStatus(); } return { running: false }; }); ipcMain.handle("inspection:scheduler:status", () => { return _scheduler ? _scheduler.getStatus() : { running: false }; }); ipcMain.handle("inspection:scheduler:scanNow", (_e, hostId) => { if (!_scheduler) return { error: "调度器未启动" }; return _scheduler.scanDeviceNow(hostId); }); ipcMain.handle("inspection:scheduler:scanAllNow", () => { if (!_scheduler) return { error: "调度器未启动" }; return _scheduler.scanAllNow(); }); console.log("[Inspection] IPC handlers 已注册"); return { getScheduler: () => _scheduler, createScheduler: (opts) => { _scheduler = new InspectionScheduler({ readFn, writeFn, execFn: execCommand, ...opts, }); return _scheduler; }, }; } module.exports = { registerInspectionHandlers, getLabels, };