CH-J Toolsمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J Tools

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / Server Manager 1704 فایل
0.1.2-beta افزوده‌شده: · ساخته‌شده:
metrics.rs افزوده‌شده: تاریخ ثبت نشده است ۴۲٫۲ KB · 806 خط
کد دانلود فایل
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.
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::ssh::platform::detect_platform;
7use crate::core::ssh::session::{SessionManager, SessionRecord};
8use crate::core::ssh::windows_exec::run_windows_script;
9use serde_json::{json, Map, Value};
10use std::sync::Arc;
11use std::time::Duration;
13const LIST_PREFIXES: [&str; 9] = ["networkInterface=", "meminfo=", "swapArea=", "vmstat=", "zram=", "zswap=", "cpuPackage=", "gpu=", "nvidiaGpu="];
15/// JS `Number(value)` for metric strings ("" ← 0, invalid ← NaN).
16fn 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)
24pub 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 }
29fn metric_value(n: f64) -> Value {
30 if n.fract() == 0.0 && n.abs() < 9.0e15 { json!(n as i64) } else { json!(n) }
33pub 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 }
39fn parse_core_types(value: &str) -> Vec<Value> {
40 value
41 .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()
55/// A sensor value or null; `[N/A]` and `[Not Supported]` from nvidia-smi are not measurements.
56fn 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 }
66fn parse_cpu_packages(lines: &[&str]) -> Vec<Value> {
67 let mut packages: Vec<Value> = lines
68 .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 packages
89fn 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": null
94 })
97/// One `nvidia-smi --query-gpu` CSV row (the same query on Linux and Windows).
98fn 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");
115/// `0000:01:00.0` from nvidia-smi's `00000000:01:00.0`, to match the sysfs device name.
116fn 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()
121/// Virtual and BMC adapters that lspci's database often lacks (or lspci is not installed at all).
122fn 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 })
137fn 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 })
154/// lspci prints `Vendor 1234 Device 1111` for IDs missing from its database.
155fn 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 }
167fn parse_gpus(lines: &[&str]) -> Vec<Value> {
168 let mut gpus: Vec<Value> = lines
169 .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 gpu
194 })
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 gpus
211fn parse_interfaces(lines: &[&str], default_interface: &str) -> Vec<Value> {
212 lines
213 .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_interface
229 })
230 })
231 .collect()
234pub 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> = lines
261 .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> = lines
280 .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 })
342pub 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 })
395// ---------------------------------------------------------------------------------------------
396// Windows
397// ---------------------------------------------------------------------------------------------
399fn 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 }
407fn 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()
416fn 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 }
424pub 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))
434fn 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 }
445fn opt(v: Option<f64>) -> Value {
446 v.map(metric_value).unwrap_or(Value::Null)
449fn 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": null
459 })).collect()
460 }).unwrap_or_default()
463/// Win32_VideoController lists the adapters; nvidia-smi rows join by name, in order. The Windows GPU
464/// counters are keyed by an adapter LUID that WMI does not link to a controller, so they count only
465/// when one real adapter and one LUID leave no doubt. Otherwise the value stays unknown.
466fn 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 gpu
488 }).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 gpus
508pub 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 files
516 .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> = raw
526 .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> = adapters
570 .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_index
592 })
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": interfaces
637 },
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 })
642pub 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 }
665async 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))
689#[cfg(test)]
690mod 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\
712gpu=0000:01:00.0|0x10de|0x2204|nvidia|NVIDIA Corporation GA102|||||||||\n\
713gpu=0000:41:00.0|0x10de|0x2204|nvidia|NVIDIA Corporation GA102|||||||||\n\
714gpu=0000:03:00.0|0x1002|0x73bf|amdgpu|AMD Navi 21|42|7|17163091968|1073741824|55000|120000000|250000000|1800000000|128\n\
715gpu=0000:05:00.0|0x10de|0x1eb8|vfio-pci|NVIDIA Tesla T4|||||||||\n\
716gpu=0000:07:00.0|0x1a03|0x2000|ast|ASPEED Graphics|||||||||\n\
717nvidiaGpu=00000000:01:00.0|NVIDIA GeForce RTX 3090|88|40|24576|20000|71|310.5|350.00|1950|65|550.54\n\
718nvidiaGpu=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\
745gpu=0000:00:03.0|0x1234|0x1111|bochs-drm||||||||||\n\
746gpu=0000:00:04.0|0x1b36|0x0100|qxl|Vendor 1b36 Device 0100|||||||||\n\
747gpu=0000:00:05.0|0x1af4|0x1050|virtio-pci|Red Hat, Inc. Virtio 1.0 GPU|||||||||\n\
748gpu=0000:00:06.0|0x10de|0xffff||Vendor 10de Device ffff|||||||||\n\
749gpu=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(&current)), 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 }
SHA-2563c265f76dc4a069efd54e121b124923fce613b5053672225e0a360f4bd680980

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانیac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
فایل‌ها