/** * 巡检调度器 Hook(前端侧) * * 为什么调度器在前端?因为 vault 主机数据在渲染进程的 localStorage 里, * 主进程拿不到完整的主机连接信息(密码/私钥)。 * * 调度器职责: * - 每 30 秒检查一次哪些设备到期需要巡检 * - 按并发限制依次调用主进程的 inspection:runInspection 执行 * - 维护运行状态、上次执行时间 */ import { useRef, useCallback, useEffect, useState, useMemo } from "react"; import { inspectionExecApi, inspectionDeviceApi } from "../../infrastructure/inspection/inspectionApi"; import type { Host } from "../../domain/models"; import type { SSHKey } from "../../domain/models/key"; import type { DeviceExtension } from "../../infrastructure/inspection/inspectionApi"; const POLL_INTERVAL_MS = 30 * 1000; // 30 秒轮询 const DEFAULT_CONCURRENCY = 3; // 默认并发数 const SYS_DEFAULT_INTERVAL = 5; // 系统默认周期(分钟) const TIER_DEFAULT_INTERVAL: Record = { core: 5, aggregation: 10, access: 15, }; export interface SchedulerStatus { running: boolean; totalDevices: number; activeDevices: number; lastScanAt: number | null; runningCount: number; } interface LastRunInfo { [hostId: string]: number; // timestamp } /** * 计算设备的有效巡检周期(分钟) * 优先级:设备自定义 > 分级默认 > 系统默认 */ function getEffectiveInterval(ext: DeviceExtension | null): number { if (!ext || !ext.inspection_enabled) return 0; if (ext.inspection_interval && ext.inspection_interval > 0) { return ext.inspection_interval; } if (ext.tier && TIER_DEFAULT_INTERVAL[ext.tier]) { return TIER_DEFAULT_INTERVAL[ext.tier]; } return SYS_DEFAULT_INTERVAL; } /** * 从 Host + keys 拼出巡检需要的连接配置 */ function buildHostConfig(host: Host, keys: SSHKey[]): { id: string; label: string; hostname: string; port: number; username: string; password?: string; privateKeyContent?: string; passphrase?: string; } { const config: any = { id: host.id, label: host.label, hostname: host.hostname, port: host.port || 22, username: host.username, }; // 密码 if (host.password) { config.password = host.password; } // 私钥(通过 identityFileId 关联) if (host.identityFileId) { const key = keys.find((k) => k.id === host.identityFileId); if (key && key.privateKey) { config.privateKeyContent = key.privateKey; if (key.passphrase) { config.passphrase = key.passphrase; } } } return config; } export function useInspectionScheduler(hosts: Host[], keys: SSHKey[]) { const [status, setStatus] = useState({ running: false, totalDevices: 0, activeDevices: 0, lastScanAt: null, runningCount: 0, }); const timerRef = useRef(null); const lastRunRef = useRef({}); const runningRef = useRef(false); const runningCountRef = useRef(0); const hostsRef = useRef(hosts); const keysRef = useRef(keys); // 实时更新 hosts/keys 引用 useEffect(() => { hostsRef.current = hosts; }, [hosts]); useEffect(() => { keysRef.current = keys; }, [keys]); /** * 执行一次单设备巡检 */ const runDeviceInspection = useCallback( async (hostId: string, catalogId?: string) => { const host = hostsRef.current.find((h) => h.id === hostId); if (!host) { console.warn(`[InspectionScheduler] 未找到主机: ${hostId}`); return null; } const hostConfig = buildHostConfig(host, keysRef.current); const actualCatalogId = catalogId || "linux_server"; runningCountRef.current += 1; setStatus((s) => ({ ...s, runningCount: runningCountRef.current })); try { const result = await inspectionExecApi.runInspection( hostId, hostConfig, actualCatalogId ); lastRunRef.current[hostId] = Date.now(); return result; } catch (e: any) { console.error(`[InspectionScheduler] 巡检失败 ${hostId}:`, e.message); return { success: false, error: e.message }; } finally { runningCountRef.current = Math.max(0, runningCountRef.current - 1); setStatus((s) => ({ ...s, runningCount: runningCountRef.current })); } }, [] ); /** * 主循环:检查到期设备并执行 */ const scanLoop = useCallback(async (hosts?: typeof hosts) => { // 如果还没启动,自动启动调度器 if (!runningRef.current) { runningRef.current = true; setStatus((s) => ({ ...s, running: true })); console.log("[InspectionScheduler] 调度器自动启动"); } // 使用传入的 hosts 参数,或者用 ref const currentHosts = hosts ?? hostsRef.current; // hosts 还没加载,跳过 if (!currentHosts || currentHosts.length === 0) { console.log('[InspectionScheduler] hosts 还未加载,当前 hosts:', currentHosts?.length); return; } console.log('[InspectionScheduler] 开始巡检,设备数:', currentHosts.length); const allExts = await inspectionDeviceApi.getAll(); const extsMap = allExts || {}; // Record // 调试:统计启用了巡检的设备数量 let enabledCount = 0; for (const host of currentHosts) { const ext = extsMap[host.id]; if (ext?.inspection_enabled) enabledCount++; } console.log('[InspectionScheduler] 扩展配置数量:', Object.keys(extsMap).length, '| 启用巡检设备数:', enabledCount); const now = Date.now(); const dueDevices: { hostId: string; catalogId: string }[] = []; for (const host of currentHosts) { const ext = extsMap[host.id]; if (!ext || !ext.inspection_enabled) continue; const intervalMin = getEffectiveInterval(ext); if (intervalMin <= 0) { console.log('[InspectionScheduler] 跳过(周期为0):', host.label, '| intervalMin:', intervalMin); continue; } const lastRun = lastRunRef.current[host.id] || ext.last_scan_at || 0; const intervalMs = intervalMin * 60 * 1000; const elapsed = now - lastRun; const needRun = elapsed >= intervalMs; console.log('[InspectionScheduler] 设备:', host.label, '| 周期:', intervalMin, 'min | 距上次:', Math.round(elapsed/60000), 'min | 需要巡检:', needRun); if (needRun) { dueDevices.push({ hostId: host.id, catalogId: ext.catalog_id || "linux_server", }); } } console.log('[InspectionScheduler] 到期设备数:', dueDevices.length); if (dueDevices.length === 0) { setStatus((s) => ({ ...s, lastScanAt: now })); return; } // 并发执行(简单的并发控制) const queue = [...dueDevices]; const workers: Promise[] = []; const processQueue = async () => { while (queue.length > 0) { const device = queue.shift(); if (!device) break; await runDeviceInspection(device.hostId, device.catalogId); } }; for (let i = 0; i < Math.min(DEFAULT_CONCURRENCY, dueDevices.length); i++) { workers.push(processQueue()); } await Promise.all(workers); setStatus((s) => ({ ...s, lastScanAt: now })); }, [runDeviceInspection]); /** * 启动调度器 */ const start = useCallback(() => { if (runningRef.current) return; runningRef.current = true; // 立即执行一次 scanLoop(); // 定时轮询 timerRef.current = setInterval(() => { scanLoop(); }, POLL_INTERVAL_MS); setStatus((s) => ({ ...s, running: true })); console.log("[InspectionScheduler] 已启动"); }, [scanLoop]); /** * 停止调度器 */ const stop = useCallback(() => { runningRef.current = false; if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } setStatus((s) => ({ ...s, running: false })); console.log("[InspectionScheduler] 已停止"); }, []); /** * 立即巡检指定设备 */ const scanNow = useCallback( async (hostId: string, catalogId?: string) => { return await runDeviceInspection(hostId, catalogId); }, [runDeviceInspection] ); // 组件卸载时清理 useEffect(() => { return () => { if (timerRef.current) { clearInterval(timerRef.current); } runningRef.current = false; }; }, []); // 计算状态 useEffect(() => { const activeCount = hosts.filter((h) => { // 这里没法同步拿到 extensions,先按 hosts 总数显示 return true; }).length; setStatus((s) => ({ ...s, totalDevices: hosts.length, activeDevices: activeCount, })); }, [hosts]); return useMemo( () => ({ status, start, stop, scanNow, scanLoop, }), [status, start, stop, scanNow, scanLoop] ); }