"use strict"; /** * Windows 服务器采集器 * * 采集指标: * - cpu_usage CPU 使用率(%) * - memory_usage 内存使用率(%) * - disk_usage 磁盘使用率(%) * - process_count 进程数 * - uptime 运行时间 * - service_status 关键服务状态 * * 实现说明: * 使用 PowerShell Base64 EncodedCommand 方式执行,与 NetMesh System Manager 一致。 * 传入 exec 函数执行远程命令(由调度器注入)。 */ const BaseAdapter = require("./baseAdapter.cjs"); class WindowsServerAdapter extends BaseAdapter { constructor(connectionInfo, options = {}) { super(connectionInfo, options); this.adapterName = "windows_server"; if (options.execFn) { this.execute = options.execFn; } } getSupportedMetrics() { return [ "cpu_usage", "memory_usage", "disk_usage", "process_count", "uptime", "service_status", ]; } async collectMetrics() { const metrics = {}; const raw = {}; const DEBUG = process.env.INSPECTION_DEBUG === "1"; // 1. CPU + 内存 + 运行时间(快速,独立执行) try { const script = this._buildFastStatsScript(); const cmd = this._buildPowerShellCmd(script); if (DEBUG) console.log("[WinAdapter] 执行命令:", cmd); raw.stats = await this.execute(cmd); if (DEBUG) console.log("[WinAdapter] 原始输出:", raw.stats); const parsed = this._parseStats(raw.stats); if (DEBUG) console.log("[WinAdapter] 解析结果:", parsed); Object.assign(metrics, parsed); } catch (e) { raw.stats_error = e.message; if (DEBUG) console.error("[WinAdapter] 采集失败:", e.message); } // 2. 磁盘使用率(较慢,单独执行,超时放宽到 30s) try { const diskScript = this._buildDiskScript(); const diskCmd = this._buildPowerShellCmd(diskScript); if (DEBUG) console.log("[WinAdapter] 磁盘命令:", diskCmd); raw.disk = await this.execute(diskCmd); if (DEBUG) console.log("[WinAdapter] 磁盘原始输出:", raw.disk); metrics.disk_usage = this._parseDisk(raw.disk); } catch (e) { raw.disk_error = e.message; if (DEBUG) console.error("[WinAdapter] 磁盘采集失败:", e.message); } // 3. 关键服务状态 try { const serviceScript = this._buildServiceScript(); const cmd = this._buildPowerShellCmd(serviceScript); if (DEBUG) console.log("[WinAdapter] 服务命令:", cmd); raw.services = await this.execute(cmd); if (DEBUG) console.log("[WinAdapter] 服务原始输出:", raw.services); metrics.service_status = this._parseServices(raw.services); } catch (e) { metrics.service_status = []; raw.services_error = e.message; if (DEBUG) console.error("[WinAdapter] 服务采集失败:", e.message); } return metrics; } /** * 快速统计:CPU + 内存(使用更快的 WMI 查询) */ _buildFastStatsScript() { return ` try { $cpu = (Get-WmiObject Win32_Processor).LoadPercentage; if (-not $cpu) { $cpu = 0 } } catch { $cpu = 0 } try { $os = Get-WmiObject Win32_OperatingSystem; $total = [math]::Round($os.TotalVisibleMemorySize/1MB,0); $free = [math]::Round($os.FreePhysicalMemory/1MB,0); $usage = if ($total -gt 0) { [math]::Round(($total-$free)/$total*100,1) } else { 0 } } catch { $total = 0; $usage = 0; $free = 0 } @{ cpu = $cpu; mem_usage = $usage; mem_total = $total; mem_free = $free } | ConvertTo-Json -Compress `.trim(); } /** 磁盘使用率(只查 C: 盘) */ _buildDiskScript() { return ` try { $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" if ($disk -and $disk.Size -gt 0) { $s = [math]::Round($disk.Size/1GB,1); $f = [math]::Round($disk.FreeSpace/1GB,1); $u = [math]::Round(($disk.Size-$disk.FreeSpace)/$disk.Size*100,1) @{ d='C:'; s=$s; f=$f; u=$u } | ConvertTo-Json -Compress } else { 'null' } } catch { 'null' } `.trim(); } _parseDisk(stdout) { try { const json = this._extractJson(stdout); if (!json || json === "null" || json === null) return []; return [{ drive: json.d || "C:", size_gb: json.s || 0, free_gb: json.f || 0, usage_percent: json.u || 0, }]; } catch { return []; } } /** * 构建 PowerShell 命令:UTF-16LE Base64 编码 + -EncodedCommand * Node.js Buffer.from(str, "utf16le") 是最可靠的编码方式 */ _buildPowerShellCmd(script) { // 用 Node.js 原生 Buffer 进行 UTF-16LE 编码,再转 Base64 // UTF-16LE 编码(不带 BOM),然后 Base64 const utf16le = Buffer.from(script, "utf16le"); const encoded = utf16le.toString("base64"); return `powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`; } _buildServiceScript() { return ` $ErrorActionPreference = 'SilentlyContinue' # 关键服务 $services = @( "wuauserv", # Windows Update "Spooler", # Print Spooler "TermService", # Remote Desktop "DHCP", # DHCP Client "DNScache" # DNS Client ) $results = foreach ($svc in $services) { $s = Get-Service -Name $svc -ErrorAction SilentlyContinue if ($s) { [PSCustomObject]@{ name = $s.Name display_name = $s.DisplayName status = $s.Status.ToString() start_type = $s.StartType.ToString() } } } $results | ConvertTo-Json -Depth 3 `.trim(); } _parseStats(stdout) { try { const json = this._extractJson(stdout); if (!json) return {}; const result = {}; if (json.cpu !== undefined) result.cpu_usage = json.cpu; if (json.mem_usage !== undefined) result.memory_usage = json.mem_usage; if (json.mem_total !== undefined && json.mem_total > 0) { result.memory = { total_mb: json.mem_total, used_mb: Math.round((json.mem_usage / 100) * json.mem_total), }; } return result; } catch (e) { return {}; } } _parseServices(stdout) { try { const json = this._extractJson(stdout); if (!json) return []; return Array.isArray(json) ? json : [json]; } catch (e) { return []; } } /** * 从 PowerShell 输出中提取 JSON(可能有多余的警告/文本) */ _extractJson(text) { if (!text) return null; const start = text.indexOf("{"); const startArr = text.indexOf("["); const realStart = startArr === -1 ? start : (start === -1 ? startArr : Math.min(start, startArr)); if (realStart === -1) return null; let depth = 0; let end = -1; const open = text[realStart]; const close = open === "{" ? "}" : "]"; for (let i = realStart; i < text.length; i++) { if (text[i] === open) depth++; else if (text[i] === close) { depth--; if (depth === 0) { end = i; break; } } } if (end === -1) return null; try { return JSON.parse(text.substring(realStart, end + 1)); } catch (e) { return null; } } } module.exports = WindowsServerAdapter;