Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
872 Dateien
core-20261030T220507Z-61e4558d889f
Hinzugefügt: · Erstellt:
1
//! System metrics for the System Monitor module (port of `readSystemMetrics`,2
//! `parseSystemMetricsOutput`, `parseLinuxMemoryDetail` and `sessions/windowsMetrics.js`).3
//! The Linux script is never sent to a Windows shell: the platform is detected first.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::ssh::platform::detect_platform;7
use crate::core::ssh::session::{SessionManager, SessionRecord};8
use crate::core::ssh::windows_exec::run_windows_script;9
use serde_json::{json, Map, Value};10
use std::sync::Arc;11
use std::time::Duration;13
pub const METRICS_COMMAND: &str = include_str!("scripts/linux_metrics.sh");14
pub const WINDOWS_METRICS_SCRIPT: &str = include_str!("scripts/windows_metrics.ps1");15
const LIST_PREFIXES: [&str; 9] = ["networkInterface=", "meminfo=", "swapArea=", "vmstat=", "zram=", "zswap=", "cpuPackage=", "gpu=", "nvidiaGpu="];17
/// JS `Number(value)` for metric strings ("" → 0, invalid → NaN).18
fn js_number(value: &str) -> f64 {19
let trimmed = value.trim();20
if trimmed.is_empty() {21
return 0.0;22
}23
trimmed.parse::<f64>().unwrap_or(f64::NAN)24
}26
pub fn metric_number(value: &str, min: f64, max: f64) -> f64 {27
let n = js_number(value);28
if n.is_finite() { n.max(min).min(max) } else { min }29
}31
fn metric_value(n: f64) -> Value {32
if n.fract() == 0.0 && n.abs() < 9.0e15 { json!(n as i64) } else { json!(n) }33
}35
pub fn metric_text(value: &str, fallback: &str, max: usize) -> String {36
let replaced: String = value.chars().map(|c| if (c as u32) < 0x20 || c as u32 == 0x7f || c == '|' { ' ' } else { c }).collect();37
let normalized: String = replaced.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(max).collect();38
if normalized.is_empty() { fallback.to_string() } else { normalized }39
}41
fn parse_core_types(value: &str) -> Vec<Value> {42
value43
.split(',')44
.take(16)45
.filter_map(|entry| {46
let index = entry.rfind(':')?;47
if index == 0 {48
return None;49
}50
let name = metric_text(&entry[..index], "", 80);51
let count = metric_number(&entry[index + 1..], 0.0, 4096.0).floor();52
(!name.is_empty() && count > 0.0).then(|| json!({ "name": name, "count": count as i64 }))53
})54
.collect()55
}57
/// A sensor value or null; `[N/A]` and `[Not Supported]` from nvidia-smi are not measurements.58
fn sensor(value: &str, scale: f64, max: f64) -> Value {59
match value.trim().parse::<f64>() {60
Ok(n) if n.is_finite() && n >= 0.0 => {61
let scaled = n * scale;62
metric_value((scaled.min(max) * 10.0).round() / 10.0)63
}64
_ => Value::Null,65
}66
}68
fn parse_cpu_packages(lines: &[&str]) -> Vec<Value> {69
let mut packages: Vec<Value> = lines70
.iter()71
.filter(|l| l.starts_with("cpuPackage="))72
.take(64)73
.map(|line| {74
let f: Vec<&str> = line["cpuPackage=".len()..].split('|').collect();75
let get = |i: usize| f.get(i).copied().unwrap_or("");76
json!({77
"id": metric_text(get(0), "0", 16),78
"model": metric_text(get(1), "", 240),79
"logicalCores": metric_number(get(2), 0.0, 4096.0).floor() as i64,80
"physicalCores": metric_number(get(3), 0.0, 4096.0).floor() as i64,81
"usagePercent": sensor(get(4), 1.0, 100.0),82
"frequencyMhz": sensor(get(5), 1.0, 1.0e6),83
"temperatureC": sensor(get(6), 0.001, 400.0)84
})85
})86
.collect();87
packages.sort_by_key(|p| p["id"].as_str().and_then(|id| id.parse::<i64>().ok()).unwrap_or(i64::MAX));88
packages89
}91
fn empty_gpu(id: &str) -> Value {92
json!({93
"id": id, "name": "", "vendorId": "", "deviceId": "", "driver": "", "driverVersion": "", "source": "none",94
"usagePercent": null, "memoryUsagePercent": null, "memoryTotal": null, "memoryUsed": null,95
"temperatureC": null, "powerW": null, "powerLimitW": null, "clockMhz": null, "fanPercent": null96
})97
}99
/// One `nvidia-smi --query-gpu` CSV row (the same query on Linux and Windows).100
fn apply_nvidia_row(gpu: &mut Value, fields: &[&str]) {101
let get = |i: usize| fields.get(i).copied().unwrap_or("");102
gpu["name"] = json!(metric_text(get(1), "NVIDIA GPU", 160));103
gpu["usagePercent"] = sensor(get(2), 1.0, 100.0);104
gpu["memoryUsagePercent"] = sensor(get(3), 1.0, 100.0);105
gpu["memoryTotal"] = sensor(get(4), 1_048_576.0, f64::MAX);106
gpu["memoryUsed"] = sensor(get(5), 1_048_576.0, f64::MAX);107
gpu["temperatureC"] = sensor(get(6), 1.0, 400.0);108
gpu["powerW"] = sensor(get(7), 1.0, 1.0e6);109
gpu["powerLimitW"] = sensor(get(8), 1.0, 1.0e6);110
gpu["clockMhz"] = sensor(get(9), 1.0, 1.0e6);111
gpu["fanPercent"] = sensor(get(10), 1.0, 100.0);112
gpu["driverVersion"] = json!(metric_text(get(11), "", 40));113
gpu["driver"] = json!("nvidia");114
gpu["source"] = json!("nvidia-smi");115
}117
/// `0000:01:00.0` from nvidia-smi's `00000000:01:00.0`, to match the sysfs device name.118
fn pci_id(bus_id: &str) -> String {119
let lower = bus_id.trim().to_lowercase();120
lower.get(lower.len().saturating_sub(12)..).unwrap_or(&lower).to_string()121
}123
/// Virtual and BMC adapters that lspci's database often lacks (or lspci is not installed at all).124
fn known_gpu_name(vendor: &str, device: &str) -> Option<&'static str> {125
Some(match (vendor, device) {126
("1234", "1111") => "QEMU Standard VGA",127
("1b36", "0100") => "QEMU QXL VGA",128
("1af4", "1050") => "Red Hat Virtio 1.0 GPU",129
("1013", "00b8") => "Cirrus Logic GD 5446 (QEMU)",130
("15ad", "0405") => "VMware SVGA II",131
("15ad", "0406") => "VMware SVGA 3D",132
("80ee", "beef") => "VirtualBox Graphics Adapter",133
("1414", "5353") => "Microsoft Hyper-V Virtual VGA",134
("1a03", "2000") => "ASPEED Graphics",135
_ => return None,136
})137
}139
fn gpu_vendor_name(vendor: &str) -> Option<&'static str> {140
Some(match vendor {141
"10de" => "NVIDIA",142
"1002" => "AMD",143
"8086" => "Intel",144
"1a03" => "ASPEED",145
"102b" => "Matrox",146
"1af4" | "1b36" => "Red Hat",147
"1234" => "QEMU",148
"15ad" => "VMware",149
"80ee" => "VirtualBox",150
"1013" => "Cirrus Logic",151
"1414" => "Microsoft",152
_ => return None,153
})154
}156
/// lspci prints `Vendor 1234 Device 1111` for IDs missing from its database.157
fn gpu_display_name(reported: &str, vendor: &str, device: &str) -> String {158
let unnamed = reported.is_empty() || (reported.starts_with("Vendor ") && reported.contains(" Device "));159
if !unnamed {160
return reported.to_string();161
}162
match (known_gpu_name(vendor, device), gpu_vendor_name(vendor)) {163
(Some(name), _) => name.to_string(),164
(None, Some(name)) => format!("{name} GPU"),165
_ => reported.to_string(),166
}167
}169
fn parse_gpus(lines: &[&str]) -> Vec<Value> {170
let mut gpus: Vec<Value> = lines171
.iter()172
.filter(|l| l.starts_with("gpu="))173
.take(32)174
.map(|line| {175
let f: Vec<&str> = line["gpu=".len()..].split('|').collect();176
let get = |i: usize| f.get(i).copied().unwrap_or("");177
let mut gpu = empty_gpu(&metric_text(get(0), "", 32));178
gpu["vendorId"] = json!(metric_text(get(1), "", 16).trim_start_matches("0x"));179
gpu["deviceId"] = json!(metric_text(get(2), "", 16).trim_start_matches("0x"));180
gpu["driver"] = json!(metric_text(get(3), "", 40));181
let (vendor, device) = (metric_text(get(1), "", 16).trim_start_matches("0x").to_lowercase(), metric_text(get(2), "", 16).trim_start_matches("0x").to_lowercase());182
gpu["name"] = json!(gpu_display_name(&metric_text(get(4), "", 160), &vendor, &device));183
gpu["usagePercent"] = sensor(get(5), 1.0, 100.0);184
gpu["memoryUsagePercent"] = sensor(get(6), 1.0, 100.0);185
gpu["memoryTotal"] = sensor(get(7), 1.0, f64::MAX);186
gpu["memoryUsed"] = sensor(get(8), 1.0, f64::MAX);187
gpu["temperatureC"] = sensor(get(9), 0.001, 400.0);188
gpu["powerW"] = sensor(get(10), 0.000_001, 1.0e6);189
gpu["powerLimitW"] = sensor(get(11), 0.000_001, 1.0e6);190
gpu["clockMhz"] = sensor(get(12), 0.000_001, 1.0e6);191
gpu["fanPercent"] = sensor(get(13), 100.0 / 255.0, 100.0);192
if ["usagePercent", "memoryTotal", "temperatureC", "powerW", "clockMhz"].iter().any(|k| !gpu[*k].is_null()) {193
gpu["source"] = json!("sysfs");194
}195
gpu196
})197
.collect();198
for line in lines.iter().filter(|l| l.starts_with("nvidiaGpu=")).take(32) {199
let fields: Vec<&str> = line["nvidiaGpu=".len()..].split('|').collect();200
let id = pci_id(fields.first().copied().unwrap_or(""));201
match gpus.iter_mut().find(|gpu| gpu["id"] == id.as_str()) {202
Some(gpu) => apply_nvidia_row(gpu, &fields),203
None => {204
let mut gpu = empty_gpu(&id);205
apply_nvidia_row(&mut gpu, &fields);206
gpus.push(gpu);207
}208
}209
}210
gpus211
}213
fn parse_interfaces(lines: &[&str], default_interface: &str) -> Vec<Value> {214
lines215
.iter()216
.filter(|l| l.starts_with("networkInterface="))217
.take(64)218
.map(|line| {219
let fields: Vec<&str> = line["networkInterface=".len()..].split('|').collect();220
let get = |i: usize| fields.get(i).copied().unwrap_or("");221
let name = metric_text(get(0), "unknown", 64);222
let addresses: Vec<String> = get(5).split(',').take(16).map(|a| metric_text(a, "", 128)).filter(|a| !a.is_empty()).collect();223
json!({224
"name": name,225
"state": metric_text(get(1), "unknown", 32).to_lowercase(),226
"mac": metric_text(get(2), "", 64),227
"rxBytes": metric_value(metric_number(get(3), 0.0, f64::MAX)),228
"txBytes": metric_value(metric_number(get(4), 0.0, f64::MAX)),229
"addresses": addresses,230
"isDefault": name == default_interface231
})232
})233
.collect()234
}236
pub fn parse_linux_memory_detail(lines: &[&str]) -> Value {237
let mut meminfo: Map<String, Value> = Map::new();238
for line in lines.iter().filter(|l| l.starts_with("meminfo=")) {239
let mut parts = line[8..].splitn(2, '|');240
let (key, value) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""));241
let n = js_number(value);242
if !key.is_empty() && key.chars().all(|c| c.is_ascii_alphabetic() || c == '_' || c == '(' || c == ')') && n.is_finite() {243
meminfo.insert(key.into(), metric_value(n));244
}245
}246
let total = meminfo.get("MemTotal").and_then(Value::as_f64).unwrap_or(0.0);247
if total == 0.0 {248
return Value::Null;249
}250
let get = |k: &str| meminfo.get(k).cloned().unwrap_or(Value::Null);251
let num = |k: &str| meminfo.get(k).and_then(Value::as_f64).unwrap_or(0.0);252
let used = (total - num("MemFree") - num("Buffers") - num("Cached") - num("SReclaimable")).max(0.0);253
let mut vmstat: Map<String, Value> = Map::new();254
for line in lines.iter().filter(|l| l.starts_with("vmstat=")) {255
let mut parts = line[7..].splitn(2, '|');256
let key = parts.next().unwrap_or("").to_string();257
let n = js_number(parts.next().unwrap_or(""));258
if n.is_finite() {259
vmstat.insert(key, metric_value(n));260
}261
}262
let swap_areas: Vec<Value> = lines263
.iter()264
.filter(|l| l.starts_with("swapArea="))265
.take(32)266
.map(|line| {267
let f: Vec<&str> = line[9..].split('|').collect();268
let get = |i: usize| f.get(i).copied().unwrap_or("");269
let path = get(0).replace("\\040", " ");270
let is_zram = path.strip_prefix("/dev/zram").map(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())).unwrap_or(false);271
let priority = js_number(get(4));272
json!({273
"name": metric_text(&path, "", 255),274
"type": if is_zram { "zram".to_string() } else { metric_text(get(1), "unknown", 16) },275
"size": metric_value(metric_number(get(2), 0.0, f64::MAX)),276
"used": metric_value(metric_number(get(3), 0.0, f64::MAX)),277
"priority": if priority.is_finite() { metric_value(priority) } else { Value::Null }278
})279
})280
.collect();281
let zram: Vec<Value> = lines282
.iter()283
.filter(|l| l.starts_with("zram="))284
.take(16)285
.map(|line| {286
let f: Vec<&str> = line[5..].split('|').collect();287
let get = |i: usize| f.get(i).copied().unwrap_or("");288
let stat: Vec<f64> = get(2).split(',').filter(|s| !s.is_empty()).map(js_number).collect();289
let at = |i: usize| stat.get(i).map(|v| metric_value(*v)).unwrap_or(Value::Null);290
let algorithm = metric_text(get(3), "", 32);291
json!({292
"device": metric_text(get(0), "", 32),293
"diskSize": metric_value(metric_number(get(1), 0.0, f64::MAX)),294
"originalData": at(0),295
"compressedData": at(1),296
"memoryUsed": at(2),297
"memoryLimit": stat.get(3).filter(|v| **v != 0.0 && v.is_finite()).map(|v| metric_value(*v)).unwrap_or(Value::Null),298
"algorithm": if algorithm.is_empty() { Value::Null } else { json!(algorithm) }299
})300
})301
.collect();302
let zswap = lines.iter().find(|l| l.starts_with("zswap=")).map(|line| {303
let f: Vec<&str> = line[6..].split('|').collect();304
let get = |i: usize| f.get(i).copied().unwrap_or("");305
let compressor = metric_text(get(1), "", 32);306
let pool = js_number(get(2));307
json!({308
"enabled": get(0).starts_with(['Y', 'y', '1']),309
"compressor": if compressor.is_empty() { Value::Null } else { json!(compressor) },310
"maxPoolPercent": if pool.is_finite() && pool != 0.0 { metric_value(pool) } else { Value::Null }311
})312
});313
let paging = |k: &str| vmstat.get(k).cloned().unwrap_or(Value::Null);314
json!({315
"source": "/proc/meminfo",316
"total": metric_value(total),317
"free": metric_value(num("MemFree")),318
"available": get("MemAvailable"),319
"used": metric_value(used),320
"buffers": metric_value(num("Buffers")),321
"cached": metric_value(num("Cached")),322
"shmem": get("Shmem"),323
"swapCached": get("SwapCached"),324
"slab": get("Slab"),325
"slabReclaimable": get("SReclaimable"),326
"slabUnreclaimable": get("SUnreclaim"),327
"kernelStack": get("KernelStack"),328
"pageTables": get("PageTables"),329
"vmallocUsed": get("VmallocUsed"),330
"dirty": get("Dirty"),331
"writeback": get("Writeback"),332
"commit": { "committed": get("Committed_AS"), "limit": get("CommitLimit") },333
"swapTotal": get("SwapTotal"),334
"swapFree": get("SwapFree"),335
"zswapPool": get("Zswap"),336
"zswapStored": get("Zswapped"),337
"paging": { "swapInPages": paging("pswpin"), "swapOutPages": paging("pswpout"), "majorFaults": paging("pgmajfault") },338
"swapAreas": swap_areas,339
"zram": zram,340
"zswap": zswap.unwrap_or(Value::Null)341
})342
}344
pub fn parse_system_metrics_output(stdout: &str, fallback_hostname: &str) -> Value {345
let lines: Vec<&str> = stdout.split('\n').map(|l| l.trim_end_matches('\r')).filter(|l| !l.is_empty()).collect();346
let mut values: Map<String, Value> = Map::new();347
for line in lines.iter().filter(|l| !LIST_PREFIXES.iter().any(|p| l.starts_with(p))) {348
match line.find('=') {349
Some(index) if index > 0 => values.insert(line[..index].to_string(), json!(line[index + 1..])),350
_ => values.insert(line.to_string(), json!("")),351
};352
}353
let v = |k: &str| values.get(k).and_then(Value::as_str).unwrap_or("").to_string();354
let disk: Vec<f64> = { let d = v("disk"); (if d.is_empty() { "0:0:0".to_string() } else { d }).split(':').map(|x| metric_number(x, 0.0, f64::MAX)).collect() };355
let disk_at = |i: usize| disk.get(i).copied().unwrap_or(0.0);356
let memory_total = metric_number(&v("memoryTotal"), 0.0, f64::MAX);357
let memory_available = metric_number(&v("memoryAvailable"), 0.0, memory_total);358
let swap_total = metric_number(&v("swapTotal"), 0.0, f64::MAX);359
let swap_free = metric_number(&v("swapFree"), 0.0, swap_total);360
let default_interface = metric_text(&v("networkDefault"), "", 64);361
let detail = parse_linux_memory_detail(&lines);362
let page_size = metric_number(&v("pageSize"), 0.0, (1u64 << 30) as f64);363
json!({364
"os": "linux",365
"loadAverageSupported": true,366
"pageSize": if page_size == 0.0 { Value::Null } else { metric_value(page_size) },367
"memoryDetail": detail,368
"swapAreas": detail.get("swapAreas").cloned().unwrap_or_else(|| json!([])),369
"platform": metric_text(&v("platform"), "unknown", 240),370
"kernel": metric_text(&v("kernel"), "unknown", 240),371
"hostname": metric_text(&v("hostname"), fallback_hostname, 240),372
"uptimeSeconds": metric_value(metric_number(&v("uptimeSeconds"), 0.0, f64::MAX)),373
"load1": metric_value(metric_number(&v("load1"), 0.0, f64::MAX)),374
"memory": { "total": metric_value(memory_total), "available": metric_value(memory_available) },375
"swap": { "total": metric_value(swap_total), "used": metric_value((swap_total - swap_free).max(0.0)), "free": metric_value(swap_free) },376
"disk": {377
"mount": "/",378
"total": metric_value(disk_at(0)),379
"used": metric_value(disk_at(0).min(disk_at(1))),380
"available": metric_value(disk_at(0).min(disk_at(2)))381
},382
"cpu": {383
"usagePercent": metric_value(metric_number(&v("cpuUsage"), 0.0, 100.0)),384
"architecture": metric_text(&v("cpuArchitecture"), "unknown", 240),385
"model": metric_text(&v("cpuModel"), "unknown", 240),386
"logicalCores": metric_number(&v("cpuLogical"), 0.0, 4096.0).floor() as i64,387
"physicalCores": metric_number(&v("cpuPhysical"), 0.0, 4096.0).floor() as i64,388
"sockets": metric_number(&v("cpuSockets"), 0.0, 256.0).floor() as i64,389
"coreTypes": parse_core_types(&v("cpuCoreTypes")),390
"packages": parse_cpu_packages(&lines)391
},392
"gpus": parse_gpus(&lines),393
"network": { "defaultInterface": default_interface, "interfaces": parse_interfaces(&lines, &default_interface) }394
})395
}397
// ---------------------------------------------------------------------------------------------398
// Windows399
// ---------------------------------------------------------------------------------------------401
fn finite(value: Option<&Value>) -> Option<f64> {402
match value? {403
Value::Number(n) => n.as_f64().filter(|v| v.is_finite()),404
Value::String(s) if !s.is_empty() => s.trim().parse::<f64>().ok().filter(|v| v.is_finite()),405
_ => None,406
}407
}409
fn text(value: Option<&Value>, limit: usize) -> String {410
let raw = match value {411
Some(Value::String(s)) => s.clone(),412
Some(Value::Null) | None => String::new(),413
Some(other) => other.to_string(),414
};415
raw.chars().map(|c| if (c as u32) < 0x20 || c as u32 == 0x7f { ' ' } else { c }).collect::<String>().trim().chars().take(limit).collect()416
}418
fn big(value: Option<&Value>) -> Option<i128> {419
match value? {420
Value::String(s) => s.trim().parse::<i128>().ok(),421
Value::Number(n) => n.as_i64().map(i128::from),422
_ => None,423
}424
}426
pub fn cpu_usage(previous: Option<&Value>, current: Option<&Value>) -> Option<f64> {427
let (p, c) = (previous?, current?);428
let busy = (big(c.get("busyInverse"))? - big(p.get("busyInverse"))?) as f64;429
let time = (big(c.get("time"))? - big(p.get("time"))?) as f64;430
if time <= 0.0 {431
return None;432
}433
Some(((1.0 - busy / time) * 100.0).clamp(0.0, 100.0))434
}436
fn rate(previous: Option<&Value>, current: &Value, key: &str) -> Value {437
let Some(previous) = previous else { return Value::Null };438
let Some(frequency) = finite(current.get("frequency")).filter(|f| *f != 0.0) else { return Value::Null };439
let (Some(count_now), Some(count_before), Some(t_now), Some(t_before)) = (big(current.get(key)), big(previous.get(key)), big(current.get("time")), big(previous.get("time"))) else {440
return Value::Null;441
};442
let count = (count_now - count_before) as f64;443
let seconds = (t_now - t_before) as f64 / frequency;444
if seconds > 0.0 && count >= 0.0 { json!(count / seconds) } else { Value::Null }445
}447
fn opt(v: Option<f64>) -> Value {448
v.map(metric_value).unwrap_or(Value::Null)449
}451
fn windows_cpu_packages(raw: &Value) -> Vec<Value> {452
raw.get("cpuPackages").and_then(Value::as_array).map(|list| {453
list.iter().take(64).map(|p| json!({454
"id": text(p.get("id"), 16).trim_start_matches("CPU"),455
"model": text(p.get("name"), 240),456
"logicalCores": opt(finite(p.get("logical"))),457
"physicalCores": opt(finite(p.get("cores"))),458
"usagePercent": opt(finite(p.get("load")).map(|v| v.clamp(0.0, 100.0))),459
"frequencyMhz": opt(finite(p.get("mhz")).filter(|v| *v > 0.0)),460
"temperatureC": null461
})).collect()462
}).unwrap_or_default()463
}465
/// Win32_VideoController lists the adapters; nvidia-smi rows join by name, in order. The Windows GPU466
/// counters are keyed by an adapter LUID that WMI does not link to a controller, so they count only467
/// when one real adapter and one LUID leave no doubt. Otherwise the value stays unknown.468
fn windows_gpus(raw: &Value, static_info: &Value) -> Vec<Value> {469
let controllers = static_info.get("gpus").and_then(Value::as_array).cloned().unwrap_or_default();470
let mut rows: Vec<Vec<String>> = raw.get("nvidia").and_then(Value::as_array).map(|list| {471
list.iter().filter_map(Value::as_str).map(|row| row.split(',').map(|f| f.trim().to_string()).collect()).collect()472
}).unwrap_or_default();473
let mut gpus: Vec<Value> = controllers.iter().take(32).enumerate().map(|(index, c)| {474
let name = text(c.get("name"), 160);475
let pnp = text(c.get("pnp"), 200);476
let id_part = |tag: &str| pnp.split(tag).nth(1).map(|rest| rest.chars().take(4).collect::<String>().to_lowercase()).unwrap_or_default();477
let mut gpu = empty_gpu(&format!("gpu{index}"));478
gpu["name"] = json!(name);479
gpu["vendorId"] = json!(id_part("VEN_"));480
gpu["deviceId"] = json!(id_part("DEV_"));481
gpu["driverVersion"] = json!(text(c.get("driverVersion"), 40));482
// AdapterRAM is 32-bit: a value at the ceiling only means "4 GiB or more".483
gpu["memoryTotal"] = opt(finite(c.get("adapterRam")).filter(|v| *v > 0.0 && *v < 4_292_870_144.0));484
if let Some(position) = rows.iter().position(|row| row.get(1).is_some_and(|n| n.eq_ignore_ascii_case(&name))) {485
let row = rows.remove(position);486
let fields: Vec<&str> = row.iter().map(String::as_str).collect();487
apply_nvidia_row(&mut gpu, &fields);488
}489
gpu490
}).collect();491
for row in &rows {492
let fields: Vec<&str> = row.iter().map(String::as_str).collect();493
let mut gpu = empty_gpu(&pci_id(fields.first().copied().unwrap_or("")));494
apply_nvidia_row(&mut gpu, &fields);495
gpus.push(gpu);496
}497
let real = |gpu: &Value| !gpu["vendorId"].as_str().unwrap_or("").is_empty() && gpu["vendorId"] != "1414";498
let counters = raw.get("gpuCounters").and_then(Value::as_array).cloned().unwrap_or_default();499
if counters.len() == 1 && gpus.iter().filter(|gpu| real(gpu)).count() == 1 {500
if let Some(gpu) = gpus.iter_mut().find(|gpu| real(gpu) && gpu["source"] == "none") {501
let c = &counters[0];502
gpu["usagePercent"] = opt(finite(c.get("usage")).map(|v| v.clamp(0.0, 100.0)));503
gpu["memoryUsed"] = opt(finite(c.get("dedicated")));504
gpu["source"] = json!("windows-counters");505
}506
}507
gpus508
}510
pub fn normalize_windows_metrics(raw: &Value, static_info: &Value, state: &Value, fallback_hostname: &str) -> Value {511
let s = static_info;512
let total = finite(raw.get("memoryTotal")).or_else(|| finite(s.get("memoryTotal")));513
let available = finite(raw.get("memoryAvailable"));514
let counters = raw.get("memoryCounters").filter(|c| c.is_object());515
let usage = cpu_usage(state.get("cpuRaw"), raw.get("cpuRaw"));516
let pagefiles: Option<Vec<Value>> = raw.get("pagefiles").and_then(Value::as_array).map(|files| {517
files518
.iter()519
.map(|f| {520
let mb = |k: &str| finite(f.get(k)).map(|v| v * 1_048_576.0);521
json!({ "name": text(f.get("name"), 260), "allocated": opt(mb("allocatedMb")), "used": opt(mb("currentMb")), "peak": opt(mb("peakMb")), "temporary": f.get("temporary") == Some(&Value::Bool(true)) })522
})523
.collect()524
});525
let page_total = pagefiles.as_ref().map(|p| p.iter().map(|f| f["allocated"].as_f64().unwrap_or(0.0)).sum::<f64>());526
let page_used = pagefiles.as_ref().map(|p| p.iter().map(|f| f["used"].as_f64().unwrap_or(0.0)).sum::<f64>());527
let disks: Vec<Value> = raw528
.get("disks")529
.and_then(Value::as_array)530
.cloned()531
.unwrap_or_default()532
.iter()533
.map(|d| {534
let used = match (finite(d.get("total")), finite(d.get("free"))) {535
(Some(t), Some(f)) => metric_value(t - f),536
_ => Value::Null,537
};538
json!({ "name": text(d.get("name"), 16), "total": opt(finite(d.get("total"))), "available": opt(finite(d.get("available"))), "used": used, "format": text(d.get("format"), 16), "label": text(d.get("label"), 64) })539
})540
.collect();541
let system_drive = raw.get("systemDrive").and_then(Value::as_str).filter(|s| !s.is_empty()).unwrap_or("C:").to_lowercase();542
let chosen = disks.iter().find(|d| d["name"].as_str().map(|n| n.to_lowercase()) == Some(system_drive.clone())).or_else(|| disks.first());543
let adapters: Vec<Value> = raw.get("adapters").and_then(Value::as_array).cloned().unwrap_or_default().into_iter().take(64).collect();544
let default_index = adapters.iter().position(|a| a.get("gateway") == Some(&Value::Bool(true)) && a.get("state").and_then(Value::as_str) == Some("Up"));545
let memory_detail = match (counters, total) {546
(Some(c), Some(total)) if total != 0.0 => {547
let n = |k: &str| finite(c.get(k)).unwrap_or(0.0);548
let standby = n("standbyCore") + n("standbyNormal") + n("standbyReserve");549
let in_use = (total - n("available") - n("modified")).max(0.0);550
json!({551
"source": "Win32_PerfRawData_PerfOS_Memory",552
"slices": { "inUse": metric_value(in_use), "modified": c.get("modified").cloned().unwrap_or(Value::Null), "standby": metric_value(standby), "free": c.get("freeZero").cloned().unwrap_or(Value::Null) },553
"available": c.get("available").cloned().unwrap_or(Value::Null),554
"systemCache": c.get("systemCacheResident").cloned().unwrap_or(Value::Null),555
"cacheBytes": c.get("cache").cloned().unwrap_or(Value::Null),556
"kernel": { "pagedPool": c.get("poolPaged").cloned().unwrap_or(Value::Null), "nonpagedPool": c.get("poolNonpaged").cloned().unwrap_or(Value::Null), "systemCode": c.get("systemCode").cloned().unwrap_or(Value::Null), "systemDriver": c.get("systemDriver").cloned().unwrap_or(Value::Null) },557
"commit": { "committed": c.get("committed").cloned().unwrap_or(Value::Null), "limit": c.get("commitLimit").cloned().unwrap_or(Value::Null) },558
"paging": { "pagesInPerSecond": rate(state.get("memoryCounters"), c, "pagesInput"), "pagesOutPerSecond": rate(state.get("memoryCounters"), c, "pagesOutput") }559
})560
}561
_ => Value::Null,562
};563
let build = text(s.get("build"), 40);564
let ubr = text(s.get("ubr"), 40);565
let kernel = [if build.is_empty() { String::new() } else { format!("build {build}{}", if ubr.is_empty() { String::new() } else { format!(".{ubr}") }) }, text(s.get("displayVersion"), 20)]566
.into_iter()567
.filter(|p| !p.is_empty())568
.collect::<Vec<_>>()569
.join(" ");570
let non_empty = |v: String| if v.is_empty() { Value::Null } else { Value::String(v) };571
let interfaces: Vec<Value> = adapters572
.iter()573
.enumerate()574
.map(|(index, a)| {575
let rx = finite(a.get("rx"));576
let tx = finite(a.get("tx"));577
let rxp = finite(a.get("rxPackets"));578
let txp = finite(a.get("txPackets"));579
let bytes_reported = !(rx == Some(0.0) && tx == Some(0.0) && (rxp.unwrap_or(0.0) > 0.0 || txp.unwrap_or(0.0) > 0.0));580
let state = a.get("state").and_then(Value::as_str).unwrap_or("");581
json!({582
"name": text(a.get("name"), 64),583
"description": text(a.get("description"), 120),584
"state": if state == "Up" { "up".to_string() } else { text(a.get("state"), 24).to_lowercase() },585
"mac": text(a.get("mac"), 64),586
"rxBytes": if bytes_reported { opt(rx) } else { Value::Null },587
"txBytes": if bytes_reported { opt(tx) } else { Value::Null },588
"rxPackets": opt(rxp),589
"txPackets": opt(txp),590
"bytesUnavailableReason": if bytes_reported { Value::Null } else { json!("The network driver does not report transferred bytes (packets are counted).") },591
"speedBitsPerSecond": finite(a.get("speed")).filter(|v| *v > 0.0).map(metric_value).unwrap_or(Value::Null),592
"addresses": a.get("addresses").and_then(Value::as_array).map(|list| list.iter().take(16).map(|x| Value::String(text(Some(x), 64))).collect::<Vec<_>>()).unwrap_or_default(),593
"isDefault": Some(index) == default_index594
})595
})596
.collect();597
let hostname = text(s.get("hostname"), 120);598
json!({599
"os": "windows",600
"platform": "Windows",601
"kernel": non_empty(kernel),602
"osName": non_empty(text(s.get("caption"), 120)),603
"installationType": non_empty(text(s.get("installationType"), 40)),604
"hostname": if hostname.is_empty() { fallback_hostname.to_string() } else { hostname },605
"uptimeSeconds": opt(finite(raw.get("uptimeSeconds"))),606
"load1": null,607
"loadAverageSupported": false,608
"processorQueueLength": opt(finite(raw.get("processorQueueLength"))),609
"processes": opt(finite(raw.get("processes"))),610
"memory": { "total": opt(total), "available": opt(available) },611
"memoryDetail": memory_detail,612
"swap": {613
"kind": "pagefile",614
"total": opt(page_total),615
"used": opt(page_used),616
"free": match (page_total, page_used) { (Some(t), Some(u)) => metric_value((t - u).max(0.0)), _ => Value::Null }617
},618
"pagefiles": pagefiles.map(Value::Array).unwrap_or(Value::Null),619
"disk": match chosen {620
Some(d) => json!({ "mount": d["name"], "total": d["total"], "used": d["used"], "available": d["available"] }),621
None => json!({ "mount": null, "total": null, "used": null, "available": null }),622
},623
"disks": disks,624
"cpu": {625
"usagePercent": usage.map(|u| json!(u)).unwrap_or(Value::Null),626
"usagePending": usage.is_none() && raw.get("cpuRaw").map(|v| !v.is_null()).unwrap_or(false),627
"architecture": non_empty(text(s.get("architecture"), 32)),628
"model": non_empty(text(s.get("cpuModel"), 160)),629
"logicalCores": opt(finite(s.get("logical"))),630
"physicalCores": opt(finite(s.get("physical"))),631
"sockets": opt(finite(s.get("sockets"))),632
"coreTypes": [],633
"packages": windows_cpu_packages(raw)634
},635
"gpus": windows_gpus(raw, s),636
"network": {637
"defaultInterface": default_index.map(|i| text(adapters[i].get("name"), 64)).unwrap_or_default(),638
"interfaces": interfaces639
},640
"unavailable": raw.get("errors").and_then(Value::as_array).map(|e| e.iter().take(10).map(|x| Value::String(text(Some(x), 200))).collect::<Vec<_>>()).unwrap_or_default()641
})642
}644
pub async fn read_system_metrics(manager: &SessionManager, session_id: &str) -> CoreResult<Value> {645
let record = manager.require_connected(session_id)?;646
let platform = detect_platform(manager, &record, false).await?;647
let collected_at = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);648
match platform["os"].as_str().unwrap_or("unknown") {649
"windows" => read_windows_metrics(manager, &record).await,650
"unknown" => Err(CoreError::new(651
"METRICS_PLATFORM_UNSUPPORTED",652
format!("The remote operating system could not be identified: {}.", platform["reason"].as_str().unwrap_or("no POSIX shell and no Windows PowerShell")),653
)),654
_ => {655
let output = manager.exec_fixed_default(&record, METRICS_COMMAND, Duration::from_secs(10)).await?;656
let mut metrics = parse_system_metrics_output(&output.stdout, &record.host);657
let object = metrics.as_object_mut().expect("object");658
let mut ordered = Map::new();659
ordered.insert("sessionId".into(), json!(record.session_id));660
ordered.append(object);661
ordered.insert("collectedAt".into(), json!(collected_at));662
Ok(Value::Object(ordered))663
}664
}665
}667
async fn read_windows_metrics(manager: &SessionManager, record: &Arc<SessionRecord>) -> CoreResult<Value> {668
let has_static = record.metrics_state.lock().get("static").is_some();669
let raw = run_windows_script(manager, record, WINDOWS_METRICS_SCRIPT, &json!({ "static": !has_static }), Duration::from_secs(30), 4 * 1024 * 1024, "METRICS_").await?;670
let mut state = record.metrics_state.lock();671
if let Some(static_info) = raw.get("static").filter(|v| !v.is_null()) {672
state["static"] = static_info.clone();673
}674
let static_info = state.get("static").cloned().unwrap_or(Value::Null);675
let metrics = normalize_windows_metrics(&raw, &static_info, &state, &record.host);676
if let Some(cpu) = raw.get("cpuRaw").filter(|v| !v.is_null()) {677
state["cpuRaw"] = cpu.clone();678
}679
if let Some(counters) = raw.get("memoryCounters").filter(|v| !v.is_null()) {680
state["memoryCounters"] = counters.clone();681
}682
let mut ordered = Map::new();683
ordered.insert("sessionId".into(), json!(record.session_id));684
if let Value::Object(mut object) = metrics {685
ordered.append(&mut object);686
}687
ordered.insert("collectedAt".into(), json!(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)));688
Ok(Value::Object(ordered))689
}691
#[cfg(test)]692
mod tests {693
use super::*;695
#[test]696
fn linux_output_parsing() {697
let output = "platform=Linux\nkernel=6.8.0\nhostname=web\nuptimeSeconds=100\nload1=0.5\nmemoryTotal=1000\nmemoryAvailable=2000\nswapTotal=100\nswapFree=40\ndisk=1024:512:256\ncpuUsage=150\ncpuArchitecture=x86_64\ncpuModel=Intel|X\ncpuLogical=4\ncpuPhysical=2\ncpuSockets=1\ncpuCoreTypes=P-core:2,E-core:4\nnetworkDefault=eth0\nmeminfo=MemTotal|8000\nmeminfo=MemFree|1000\nmeminfo=Buffers|500\nmeminfo=Cached|2000\nmeminfo=SReclaimable|500\nswapArea=/dev/zram0|partition|1024|0|100\nnetworkInterface=eth0|UP|aa:bb|10|20|10.0.0.1/24,fe80::1/64\npageSize=4096\n";698
let m = parse_system_metrics_output(output, "fallback");699
assert_eq!(m["memory"]["available"], 1000);700
assert_eq!(m["swap"]["used"], 60);701
assert_eq!(m["cpu"]["usagePercent"], 100);702
assert_eq!(m["cpu"]["model"], "Intel X");703
assert_eq!(m["cpu"]["coreTypes"][1]["name"], "E-core");704
assert_eq!(m["memoryDetail"]["used"], 4000);705
assert_eq!(m["swapAreas"][0]["type"], "zram");706
assert_eq!(m["network"]["interfaces"][0]["state"], "up");707
assert_eq!(m["network"]["interfaces"][0]["isDefault"], true);708
assert_eq!(m["disk"]["used"], 512);709
}711
#[test]712
fn linux_cpu_packages_and_gpus() {713
let output = "cpuPackage=1|Xeon B|8|4|20.50|2100|\ncpuPackage=0|Xeon A|16|8|75.00|3400|61000\n\714
gpu=0000:01:00.0|0x10de|0x2204|nvidia|NVIDIA Corporation GA102|||||||||\n\715
gpu=0000:41:00.0|0x10de|0x2204|nvidia|NVIDIA Corporation GA102|||||||||\n\716
gpu=0000:03:00.0|0x1002|0x73bf|amdgpu|AMD Navi 21|42|7|17163091968|1073741824|55000|120000000|250000000|1800000000|128\n\717
gpu=0000:05:00.0|0x10de|0x1eb8|vfio-pci|NVIDIA Tesla T4|||||||||\n\718
gpu=0000:07:00.0|0x1a03|0x2000|ast|ASPEED Graphics|||||||||\n\719
nvidiaGpu=00000000:01:00.0|NVIDIA GeForce RTX 3090|88|40|24576|20000|71|310.5|350.00|1950|65|550.54\n\720
nvidiaGpu=00000000:41:00.0|NVIDIA GeForce RTX 3090|[N/A]|[N/A]|24576|[N/A]|[N/A]|[Not Supported]|[N/A]|[N/A]|[N/A]|550.54\n";721
let m = parse_system_metrics_output(output, "h");722
let p = m["cpu"]["packages"].as_array().unwrap();723
assert_eq!((p.len(), p[0]["id"].as_str(), p[1]["id"].as_str()), (2, Some("0"), Some("1")));724
assert_eq!(p[0]["usagePercent"], 75);725
assert_eq!(p[0]["temperatureC"], 61);726
assert_eq!(p[1]["temperatureC"], Value::Null);727
let g = m["gpus"].as_array().unwrap();728
assert_eq!(g.len(), 5);729
let (first, second, amd, passed, bmc) = (&g[0], &g[1], &g[2], &g[3], &g[4]);730
assert_eq!(first["name"], "NVIDIA GeForce RTX 3090");731
assert_eq!((first["usagePercent"].as_f64(), first["powerW"].as_f64(), first["source"].as_str()), (Some(88.0), Some(310.5), Some("nvidia-smi")));732
assert_eq!(first["memoryTotal"], 24576u64 * 1_048_576);733
assert_eq!(second["usagePercent"], Value::Null, "[N/A] is not 0 %");734
assert_eq!(second["memoryUsed"], Value::Null);735
assert_eq!(amd["usagePercent"], 42);736
assert_eq!(amd["temperatureC"], 55);737
assert_eq!(amd["powerW"], 120);738
assert_eq!(amd["clockMhz"], 1800);739
assert_eq!(amd["fanPercent"].as_f64().unwrap().round(), 50.0);740
assert_eq!((passed["driver"].as_str(), passed["source"].as_str(), passed["usagePercent"].is_null()), (Some("vfio-pci"), Some("none"), true));741
assert_eq!(bmc["name"], "ASPEED Graphics");742
}744
#[test]745
fn emulated_gpus_get_a_name_when_lspci_has_none() {746
let output = "gpu=0000:00:02.0|0x1234|0x1111||Vendor 1234 Device 1111|||||||||\n\747
gpu=0000:00:03.0|0x1234|0x1111|bochs-drm||||||||||\n\748
gpu=0000:00:04.0|0x1b36|0x0100|qxl|Vendor 1b36 Device 0100|||||||||\n\749
gpu=0000:00:05.0|0x1af4|0x1050|virtio-pci|Red Hat, Inc. Virtio 1.0 GPU|||||||||\n\750
gpu=0000:00:06.0|0x10de|0xffff||Vendor 10de Device ffff|||||||||\n\751
gpu=0000:00:07.0|0xabcd|0x0001||Vendor abcd Device 0001|||||||||\n";752
let m = parse_system_metrics_output(output, "h");753
let names: Vec<&str> = m["gpus"].as_array().unwrap().iter().map(|g| g["name"].as_str().unwrap()).collect();754
assert_eq!(names, ["QEMU Standard VGA", "QEMU Standard VGA", "QEMU QXL VGA", "Red Hat, Inc. Virtio 1.0 GPU", "NVIDIA GPU", "Vendor abcd Device 0001"]);755
assert_eq!(m["gpus"][0]["vendorId"], "1234");756
assert_eq!(m["gpus"][0]["usagePercent"], Value::Null, "a name does not invent metrics");757
}759
#[test]760
fn linux_without_gpu_or_new_lines_is_empty() {761
let m = parse_system_metrics_output("platform=Linux\n", "h");762
assert_eq!(m["gpus"], json!([]));763
assert_eq!(m["cpu"]["packages"], json!([]));764
}766
#[test]767
fn windows_gpus_join_nvidia_and_refuse_ambiguous_counters() {768
let controller = |name: &str, pnp: &str| json!({ "name": name, "pnp": pnp, "adapterRam": 4294967295u64, "driverVersion": "1" });769
let raw = json!({770
"nvidia": ["00000000:01:00.0, NVIDIA RTX A, 50, 10, 8192, 1024, 60, 100.0, 200.0, 1500, 40, 551.0", "00000000:02:00.0, NVIDIA RTX A, 5, 1, 8192, 512, 40, 20.0, 200.0, 300, 30, 551.0"],771
"gpuCounters": [{ "luid": "a", "usage": 9, "dedicated": 5 }, { "luid": "b", "usage": 1, "dedicated": 5 }],772
"cpuPackages": [{ "id": "CPU0", "name": "EPYC", "cores": 8, "logical": 16, "load": 30, "mhz": 2500 }, { "id": "CPU1", "name": "EPYC", "cores": 8, "logical": 16, "load": null, "mhz": 2500 }]773
});774
let info = json!({ "gpus": [controller("NVIDIA RTX A", "PCI\\VEN_10DE&DEV_2204&X"), controller("NVIDIA RTX A", "PCI\\VEN_10DE&DEV_2204&Y")] });775
let m = normalize_windows_metrics(&raw, &info, &json!({}), "h");776
assert_eq!(m["gpus"][0]["usagePercent"], 50);777
assert_eq!(m["gpus"][1]["usagePercent"], 5);778
assert_eq!(m["gpus"][0]["vendorId"], "10de");779
assert_eq!(m["cpu"]["packages"][1]["id"], "1");780
assert_eq!(m["cpu"]["packages"][1]["usagePercent"], Value::Null);781
// One real adapter, one counter set: usable. Basic display adapter (1414) does not count.782
let one = json!({ "gpus": [controller("AMD X", "PCI\\VEN_1002&DEV_73BF"), controller("Microsoft Basic Display Adapter", "PCI\\VEN_1414&DEV_008C")] });783
let raw = json!({ "gpuCounters": [{ "luid": "a", "usage": 33, "dedicated": 2048 }] });784
let m = normalize_windows_metrics(&raw, &one, &json!({}), "h");785
assert_eq!((m["gpus"][0]["usagePercent"].as_f64(), m["gpus"][0]["source"].as_str()), (Some(33.0), Some("windows-counters")));786
assert_eq!(m["gpus"][0]["memoryTotal"], Value::Null, "the 32-bit ceiling is not a size");787
// Two real adapters, two counter sets: no guessing.788
let two = json!({ "gpus": [controller("AMD X", "PCI\\VEN_1002&DEV_73BF"), controller("Intel Y", "PCI\\VEN_8086&DEV_46A6")] });789
let raw = json!({ "gpuCounters": [{ "luid": "a", "usage": 33 }, { "luid": "b", "usage": 1 }] });790
let m = normalize_windows_metrics(&raw, &two, &json!({}), "h");791
assert_eq!(m["gpus"][0]["usagePercent"], Value::Null);792
}794
#[test]795
fn windows_cpu_from_raw_counters() {796
let previous = json!({"busyInverse": "1000", "time": "2000"});797
let current = json!({"busyInverse": "1500", "time": "3000"});798
assert_eq!(cpu_usage(Some(&previous), Some(¤t)), Some(50.0));799
let raw = json!({"memoryTotal": 100.0, "memoryAvailable": 40.0, "cpuRaw": current, "pagefiles": [{"name": "C:\\pagefile.sys", "allocatedMb": 1, "currentMb": 0.5}], "disks": [{"name": "C:", "total": 10, "free": 4, "available": 4}], "systemDrive": "C:", "adapters": [{"name": "Ethernet", "state": "Up", "gateway": true, "rx": 0, "tx": 0, "rxPackets": 5, "txPackets": 1}]});800
let m = normalize_windows_metrics(&raw, &json!({"build": "26100", "ubr": "1"}), &json!({"cpuRaw": previous}), "h");801
assert_eq!(m["cpu"]["usagePercent"], 50.0);802
assert_eq!(m["swap"]["total"], 1_048_576);803
assert_eq!(m["disk"]["used"], 6);804
assert_eq!(m["network"]["interfaces"][0]["rxBytes"], Value::Null);805
assert_eq!(m["kernel"], "build 26100.1");806
assert_eq!(m["load1"], Value::Null);807
}808
}SHA-256
19e909ea0d45f08f534f3f12ac1f3c5b61430d26c17d369848b0e333816f3aa7CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df