238 lines
7.0 KiB
JavaScript
238 lines
7.0 KiB
JavaScript
|
|
"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;
|