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