Files
SenMeshNet/application/state/useInspectionScheduler.ts

318 lines
8.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* 巡检调度器 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<string, number> = {
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<SchedulerStatus>({
running: false,
totalDevices: 0,
activeDevices: 0,
lastScanAt: null,
runningCount: 0,
});
const timerRef = useRef<NodeJS.Timeout | null>(null);
const lastRunRef = useRef<LastRunInfo>({});
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<string, DeviceExtension>
// 调试:统计启用了巡检的设备数量
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<any>[] = [];
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]
);
}