rigtop_core/snapshot.rs
1//! Serializable snapshot of everything rigtop displays.
2//!
3//! A [`Snapshot`] is the single unit of data exchanged between a data source
4//! (local collectors, or a remote `rigtop --agent` over SSH) and a front-end
5//! (TUI or GUI). Every type here derives `Serialize`/`Deserialize` so that the
6//! same structure can be streamed over the wire unchanged.
7//!
8//! Conventions:
9//! - Sizes are in bytes, rates in bytes per second.
10//! - Temperatures are in degrees Celsius, power in watts, fan speed in RPM.
11//! - Percentages are in the range `0.0..=100.0` (per-core values are not summed).
12//! - Values that a platform or device cannot provide are `None` (or an empty
13//! list) rather than zero, so that front-ends can hide them.
14//! - Every struct is `#[serde(default)]`: fields missing from the input take
15//! their default, so a client can read snapshots from an older or newer
16//! `rigtop --agent` as long as fields are only added.
17
18use serde::{Deserialize, Serialize};
19
20/// One full measurement of a host.
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(default)]
23pub struct Snapshot {
24 /// Wall-clock time of the measurement, in milliseconds since the Unix epoch.
25 pub timestamp_ms: u64,
26 /// Static-ish information about the host.
27 pub host: HostInfo,
28 /// CPU load, frequency and temperature.
29 pub cpu: CpuStats,
30 /// Physical memory and swap.
31 pub memory: MemoryStats,
32 /// Discrete and integrated GPUs that could be queried.
33 pub gpus: Vec<GpuStats>,
34 /// Raw hardware sensor chips (temperatures and fans), grouped by chip.
35 pub sensors: Vec<SensorChip>,
36 /// Power draw summary.
37 pub power: PowerStats,
38 /// I/O activity per physical block device.
39 pub disks: Vec<DiskIo>,
40 /// Capacity of mounted filesystems.
41 pub filesystems: Vec<FilesystemUsage>,
42 /// Software RAID state (btrfs and mdadm).
43 pub raid: RaidStats,
44 /// SMART health per drive, from files written by a privileged timer.
45 pub smart: Vec<SmartDevice>,
46 /// Network interface traffic.
47 pub networks: Vec<NetworkStats>,
48 /// Process list. May be truncated by the source (see [`Snapshot::truncate_processes`]).
49 pub processes: Vec<ProcessInfo>,
50 /// Total number of processes on the host, even if `processes` was truncated.
51 pub process_count: usize,
52 /// Linux: systemd units in the failed state (e.g. `"nginx.service"`).
53 pub failed_units: Vec<String>,
54 /// Windows: why LibreHardwareMonitor, the source of temperatures, fans
55 /// and CPU power, could not be read. `None` when it was read, when it is
56 /// disabled in the configuration, and on other platforms.
57 pub lhm_error: Option<String>,
58}
59
60impl Snapshot {
61 /// Keeps only the `limit` processes with the highest CPU usage.
62 ///
63 /// Used to reduce the payload when a host is shown in an overview rather
64 /// than in detail. Kernel threads are dropped first: the overview does not
65 /// show them, and busy `kworker`s would otherwise take places in the top
66 /// `limit` and leave the list short. `process_count` keeps the original
67 /// total.
68 pub fn truncate_processes(&mut self, limit: usize) {
69 self.processes.retain(|p| !p.kernel_thread);
70 if self.processes.len() <= limit {
71 return;
72 }
73 // Partial sort is enough: only the top `limit` entries matter.
74 self.processes
75 .select_nth_unstable_by(limit, |a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
76 self.processes.truncate(limit);
77 self.processes
78 .sort_unstable_by(|a, b| b.cpu_percent.total_cmp(&a.cpu_percent));
79 }
80}
81
82/// Host identification and uptime.
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84#[serde(default)]
85pub struct HostInfo {
86 /// Host name as reported by the OS.
87 pub hostname: String,
88 /// Human readable OS name and version (e.g. "Linux 26.04 Ubuntu").
89 pub os: String,
90 /// Kernel version string.
91 pub kernel: String,
92 /// Seconds since boot.
93 pub uptime_secs: u64,
94 /// Version of the rigtop that took the snapshot (the agent's, for a
95 /// remote host). Agents before 0.9.1 leave it out; the remote source
96 /// fills it in from the agent's greeting.
97 pub rigtop_version: String,
98}
99
100/// CPU-wide and per-core statistics.
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102#[serde(default)]
103pub struct CpuStats {
104 /// Brand string of the first CPU (e.g. "AMD Ryzen 9 9950X3D 16-Core Processor").
105 pub model: String,
106 /// Usage across all logical cores.
107 pub total_percent: f32,
108 /// Usage of each logical core, in OS order.
109 pub per_core_percent: Vec<f32>,
110 /// Current frequency of each logical core in MHz.
111 pub per_core_mhz: Vec<u64>,
112 /// 1, 5 and 15 minute load averages; `None` on platforms without them
113 /// (Windows).
114 pub load_average: Option<[f64; 3]>,
115 /// CPU temperature readings, e.g. `Tctl`, `Tccd1`, `Tccd2` on AMD or
116 /// `Package id 0`, `Core 0`.. on Intel.
117 pub temperatures: Vec<Temperature>,
118}
119
120/// Physical memory and swap usage.
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122#[serde(default)]
123pub struct MemoryStats {
124 /// Installed (visible) memory.
125 pub total_bytes: u64,
126 /// Memory in use by processes (total minus available).
127 pub used_bytes: u64,
128 /// Memory that can be allocated without swapping.
129 pub available_bytes: u64,
130 /// Completely unused memory.
131 pub free_bytes: u64,
132 /// Page cache and reclaimable slab, where the platform reports it.
133 pub cached_bytes: Option<u64>,
134 /// Total swap space.
135 pub swap_total_bytes: u64,
136 /// Swap space in use.
137 pub swap_used_bytes: u64,
138}
139
140/// A single GPU.
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
142#[serde(default)]
143pub struct GpuStats {
144 /// Position in the GPU list: NVIDIA GPUs first (their NVML index),
145 /// then AMD and Intel ones.
146 pub index: u32,
147 /// Vendor name ("NVIDIA", "AMD", ...).
148 pub vendor: String,
149 /// Marketing name of the device.
150 pub name: String,
151 /// Integrated graphics (an APU or Intel iGPU), whose power is part of
152 /// the CPU package and not reported here.
153 pub integrated: bool,
154 /// Core utilization (unknown for Intel without privileges).
155 pub utilization_percent: Option<f32>,
156 /// Memory controller utilization.
157 pub memory_utilization_percent: Option<f32>,
158 /// VRAM in use.
159 pub memory_used_bytes: Option<u64>,
160 /// Total VRAM.
161 pub memory_total_bytes: Option<u64>,
162 /// Core temperature.
163 pub temperature_c: Option<f32>,
164 /// Current board power draw.
165 pub power_w: Option<f32>,
166 /// Enforced power limit.
167 pub power_limit_w: Option<f32>,
168 /// Fan speed as a percentage of the maximum (first fan).
169 pub fan_percent: Option<f32>,
170 /// Graphics clock in MHz.
171 pub clock_mhz: Option<u32>,
172 /// Processes currently using this GPU.
173 pub processes: Vec<GpuProcess>,
174}
175
176/// GPU usage attributed to one process.
177#[derive(Debug, Clone, Default, Serialize, Deserialize)]
178#[serde(default)]
179pub struct GpuProcess {
180 /// OS process id.
181 pub pid: u32,
182 /// VRAM used by the process, if the driver reports it.
183 pub memory_bytes: Option<u64>,
184 /// Streaming multiprocessor (core) utilization by the process.
185 pub sm_percent: Option<f32>,
186}
187
188/// A hardware monitoring chip (a Linux hwmon device) and its readings.
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
190#[serde(default)]
191pub struct SensorChip {
192 /// Stable identifier, `"<driver name>:<device name>"`
193 /// (e.g. `"nct6686:nct6683.2592"`, `"nvme:nvme0"`).
194 ///
195 /// Unlike `hwmonN`, this survives reboots, so it is used as the key for
196 /// user-defined labels.
197 pub key: String,
198 /// Driver-provided chip name (e.g. `"k10temp"`, `"nvme"`, `"spd5118"`).
199 pub name: String,
200 /// Temperature channels.
201 pub temperatures: Vec<Temperature>,
202 /// Fan channels.
203 pub fans: Vec<Fan>,
204}
205
206/// One temperature channel.
207#[derive(Debug, Clone, Default, Serialize, Deserialize)]
208#[serde(default)]
209pub struct Temperature {
210 /// Stable key `"<chip key>/temp<N>"` used to look up user-defined names.
211 /// Empty for readings that do not come from a hwmon chip.
212 pub key: String,
213 /// Driver label (e.g. `"Tccd1"`), or a generic `"temp<N>"` when none exists.
214 pub label: String,
215 /// Current reading.
216 pub celsius: f32,
217 /// High threshold, if exposed.
218 pub max_celsius: Option<f32>,
219 /// Critical threshold, if exposed.
220 pub critical_celsius: Option<f32>,
221}
222
223/// One fan channel.
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225#[serde(default)]
226pub struct Fan {
227 /// Stable key `"<chip key>/fan<N>"` used to look up user-defined names.
228 pub key: String,
229 /// Driver label, or a generic `"fan<N>"` when none exists.
230 pub label: String,
231 /// Current speed (0 when stopped or not connected).
232 pub rpm: u32,
233}
234
235/// Power draw summary.
236///
237/// There is no portable way to measure whole-system power without external
238/// hardware, so the total is the sum of the CPU package and discrete GPUs,
239/// which dominate consumption on typical workstations.
240#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241#[serde(default)]
242pub struct PowerStats {
243 /// CPU package power (RAPL on Linux). `None` before the second sample or
244 /// when RAPL is unavailable.
245 pub cpu_package_w: Option<f32>,
246 /// Sum of power reported by discrete GPUs.
247 pub gpus_w: Option<f32>,
248 /// Power an AMD APU's graphics driver reports (amdgpu `PPT`). Per the
249 /// kernel documentation it covers the whole SoC, CPU cores included, so
250 /// it overlaps the CPU package and only stands in for it when RAPL cannot
251 /// be read (see [`PowerStats::cpu_or_soc_w`]).
252 pub igpu_w: Option<f32>,
253 /// `cpu_package_w` (or `igpu_w` without it) `+ gpus_w`, counting only the
254 /// parts that are available.
255 pub total_w: Option<f32>,
256 /// Laptop battery, if there is one.
257 pub battery: Option<BatteryStats>,
258}
259
260/// Below this, an APU reading is not whole-SoC power: desktop Ryzen CPUs
261/// with a small iGPU report only a few milliwatts there, while a laptop
262/// APU's SoC never idles under a watt.
263const MIN_SOC_W: f32 = 1.0;
264
265impl PowerStats {
266 /// The CPU figure to show: `("CPU", RAPL)`, else `("SoC", APU PPT)`.
267 pub fn cpu_or_soc_w(&self) -> (&'static str, Option<f32>) {
268 match (self.cpu_package_w, self.igpu_w) {
269 (Some(w), _) => ("CPU", Some(w)),
270 (None, Some(w)) if w >= MIN_SOC_W => ("SoC", Some(w)),
271 _ => ("CPU", None),
272 }
273 }
274}
275
276/// Charge and power of the system's batteries (summed when there are
277/// several), from `/sys/class/power_supply` on Linux.
278#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
279#[serde(default)]
280pub struct BatteryStats {
281 /// Charge in percent.
282 pub percent: Option<f32>,
283 /// Kernel status: `Charging`, `Discharging`, `Full`, `Not charging`...
284 pub status: String,
285 /// Charge or discharge power (not signed; see `status`).
286 pub power_w: Option<f32>,
287}
288
289impl BatteryStats {
290 /// Short form for status lines: `72% -12W` while discharging, `+20W`
291 /// while charging.
292 pub fn summary(&self) -> String {
293 let percent = self
294 .percent
295 .map_or("--%".to_owned(), |p| format!("{p:.0}%"));
296 let sign = match self.status.as_str() {
297 "Discharging" => "-",
298 "Charging" => "+",
299 _ => return percent,
300 };
301 match self.power_w {
302 Some(w) if w >= 0.5 => format!("{percent} {sign}{w:.0}W"),
303 _ => percent,
304 }
305 }
306}
307
308/// I/O activity of a block device.
309#[derive(Debug, Clone, Default, Serialize, Deserialize)]
310#[serde(default)]
311pub struct DiskIo {
312 /// Kernel device name (e.g. `"nvme0n1"`, `"sda"`).
313 pub name: String,
314 /// Read throughput.
315 pub read_bytes_per_sec: f64,
316 /// Write throughput.
317 pub write_bytes_per_sec: f64,
318 /// Fraction of time the device had I/O in flight, where the platform
319 /// reports it (Linux).
320 pub busy_percent: Option<f32>,
321 /// Drive temperature (NVMe composite sensor), if available.
322 pub temperature_c: Option<f32>,
323}
324
325/// Capacity of one filesystem.
326///
327/// Filesystems mounted at several places (e.g. btrfs subvolumes) appear once,
328/// with all mount points listed.
329#[derive(Debug, Clone, Default, Serialize, Deserialize)]
330#[serde(default)]
331pub struct FilesystemUsage {
332 /// Source device (e.g. `"/dev/nvme1n1p1"`).
333 pub device: String,
334 /// Filesystem type (e.g. `"btrfs"`, `"ext4"`).
335 pub fs_type: String,
336 /// All mount points of this filesystem, shortest first.
337 pub mount_points: Vec<String>,
338 /// Total size.
339 pub total_bytes: u64,
340 /// Space available to unprivileged users.
341 pub available_bytes: u64,
342}
343
344/// Returns true if `device` (a disk such as `"sda"`, or an NVMe controller
345/// such as `"nvme0"`) is the drive behind the block device `disk`
346/// (`"sda"`, `"nvme0n1"`).
347///
348/// `"nvme1"` does not match `"nvme10n1"`: after the controller name only the
349/// namespace suffix (`n<N>`) may follow.
350pub fn device_matches_disk(device: &str, disk: &str) -> bool {
351 match disk.strip_prefix(device) {
352 Some("") => true,
353 Some(rest) => device.starts_with("nvme") && rest.starts_with('n'),
354 None => false,
355 }
356}
357
358/// SMART health of one drive, parsed from `smartctl -j -a` output.
359///
360/// Fields a drive or protocol does not report are `None` (NVMe drives have
361/// no sector reallocation counters, ATA drives no "percentage used").
362#[derive(Debug, Clone, Default, Serialize, Deserialize)]
363#[serde(default)]
364pub struct SmartDevice {
365 /// Kernel device name (`"nvme0"`, `"sda"`). See [`device_matches_disk`].
366 pub device: String,
367 /// Protocol reported by smartctl (`"NVMe"`, `"ATA"`, `"SCSI"`).
368 pub protocol: String,
369 /// Drive model.
370 pub model: String,
371 /// Overall health self-assessment; `None` if smartctl could not tell.
372 pub passed: Option<bool>,
373 /// Current drive temperature.
374 pub temperature_c: Option<f32>,
375 /// Accumulated power-on time.
376 pub power_on_hours: Option<u64>,
377 /// NVMe wear estimate (may exceed 100).
378 pub percentage_used: Option<u32>,
379 /// NVMe spare capacity left.
380 pub available_spare_percent: Option<u32>,
381 /// NVMe spare threshold below which the drive reports a warning.
382 pub available_spare_threshold_percent: Option<u32>,
383 /// NVMe critical warning bit field (0 when healthy).
384 pub critical_warning: Option<u32>,
385 /// NVMe unrecovered data integrity errors.
386 pub media_errors: Option<u64>,
387 /// ATA attribute 5 (Reallocated_Sector_Ct), raw value.
388 pub reallocated_sectors: Option<u64>,
389 /// ATA attribute 197 (Current_Pending_Sector), raw value.
390 pub pending_sectors: Option<u64>,
391 /// ATA attribute 198 (Offline_Uncorrectable), raw value.
392 pub offline_uncorrectable: Option<u64>,
393 /// Total host writes, where the drive reports them.
394 pub data_written_bytes: Option<u64>,
395 /// When smartctl ran, in milliseconds since the Unix epoch. Lets front-ends
396 /// flag stale results (e.g. the timer stopped running).
397 pub collected_at_ms: Option<u64>,
398 /// Error reported by smartctl (e.g. the device could not be opened).
399 pub error: Option<String>,
400}
401
402impl SmartDevice {
403 /// Returns true if anything in the SMART data calls for attention.
404 pub fn needs_attention(&self) -> bool {
405 let positive = |v: Option<u64>| v.is_some_and(|v| v > 0);
406 let spare_low = match (
407 self.available_spare_percent,
408 self.available_spare_threshold_percent,
409 ) {
410 (Some(spare), Some(threshold)) => spare < threshold,
411 _ => false,
412 };
413 self.passed == Some(false)
414 || self.error.is_some()
415 || self.critical_warning.is_some_and(|w| w != 0)
416 || positive(self.media_errors)
417 || positive(self.reallocated_sectors)
418 || positive(self.pending_sectors)
419 || positive(self.offline_uncorrectable)
420 || self.percentage_used.is_some_and(|p| p >= 100)
421 || spare_low
422 }
423}
424
425/// Software RAID state.
426#[derive(Debug, Clone, Default, Serialize, Deserialize)]
427#[serde(default)]
428pub struct RaidStats {
429 /// Mounted btrfs filesystems.
430 pub btrfs: Vec<BtrfsFilesystem>,
431 /// Linux md (mdadm) arrays.
432 pub md: Vec<MdArray>,
433 /// Imported ZFS pools.
434 pub zfs: Vec<ZfsPool>,
435}
436
437/// A ZFS pool and its health.
438#[derive(Debug, Clone, Default, Serialize, Deserialize)]
439#[serde(default)]
440pub struct ZfsPool {
441 /// Pool name (e.g. `"tank"`).
442 pub name: String,
443 /// Health as `zpool status` shows it: `ONLINE`, `DEGRADED`, `FAULTED`,
444 /// `OFFLINE`, `UNAVAIL`, `REMOVED` or `SUSPENDED`.
445 pub state: String,
446}
447
448impl ZfsPool {
449 /// Whether the pool is healthy.
450 pub fn is_online(&self) -> bool {
451 self.state == "ONLINE"
452 }
453
454 /// Whether the pool lost redundancy or data is at risk (worse than an
455 /// administratively offline pool).
456 pub fn is_failing(&self) -> bool {
457 matches!(
458 self.state.as_str(),
459 "DEGRADED" | "FAULTED" | "UNAVAIL" | "REMOVED" | "SUSPENDED"
460 )
461 }
462}
463
464/// A btrfs filesystem as seen in `/sys/fs/btrfs/<fsid>`.
465#[derive(Debug, Clone, Default, Serialize, Deserialize)]
466#[serde(default)]
467pub struct BtrfsFilesystem {
468 /// Filesystem UUID.
469 pub fsid: String,
470 /// Filesystem label (may be empty).
471 pub label: String,
472 /// Mount points found in the mount table for this filesystem.
473 pub mount_points: Vec<String>,
474 /// Member block devices (e.g. `"nvme0n1"`, `"nvme1n1p1"`).
475 pub devices: Vec<String>,
476 /// Per-device error counters, keyed by btrfs device id. Each carries its
477 /// device name when it could be looked up (see [`BtrfsDeviceStats::name`]);
478 /// the list is not aligned with `devices`.
479 pub device_stats: Vec<BtrfsDeviceStats>,
480 /// Allocation per block group type (data, metadata, system).
481 pub allocations: Vec<BtrfsAllocation>,
482 /// Result of the most recent scrub, if it is known. Error counters in
483 /// `device_stats` only grow when data is read, so the scrub time tells
484 /// when all data was last verified.
485 pub scrub: Option<BtrfsScrub>,
486}
487
488/// Outcome of the last btrfs scrub (summed over devices).
489#[derive(Debug, Clone, Default, Serialize, Deserialize)]
490#[serde(default)]
491pub struct BtrfsScrub {
492 /// Start time, in milliseconds since the Unix epoch.
493 pub started_at_ms: u64,
494 /// Run time in seconds.
495 pub duration_secs: u64,
496 /// `"finished"`, `"canceled"` or `"unfinished"` (running or interrupted).
497 pub state: String,
498 /// Bytes verified.
499 pub bytes_scrubbed: u64,
500 /// Read, checksum, verify and superblock errors found.
501 pub errors: u64,
502 /// Errors that were repaired from a good copy.
503 pub corrected: u64,
504 /// Errors that could not be repaired.
505 pub uncorrectable: u64,
506}
507
508impl BtrfsFilesystem {
509 /// Returns true if any member is missing or has a non-zero error counter.
510 pub fn has_problems(&self) -> bool {
511 self.device_stats
512 .iter()
513 .any(|d| d.missing || d.total_errors() > 0)
514 }
515}
516
517/// Error counters of one btrfs member device.
518#[derive(Debug, Clone, Default, Serialize, Deserialize)]
519#[serde(default)]
520pub struct BtrfsDeviceStats {
521 /// btrfs device id (1-based).
522 pub devid: u64,
523 /// Device name (e.g. `"nvme0n1"`), from the `BTRFS_IOC_DEV_INFO` ioctl
524 /// on a mount point; empty when unknown (not mounted, device missing).
525 pub name: String,
526 /// Size of the device as btrfs uses it.
527 pub size_bytes: Option<u64>,
528 /// Space allocated on the device.
529 pub used_bytes: Option<u64>,
530 /// Whether the device is missing from the filesystem.
531 pub missing: bool,
532 /// Write I/O errors.
533 pub write_errs: u64,
534 /// Read I/O errors.
535 pub read_errs: u64,
536 /// Flush errors.
537 pub flush_errs: u64,
538 /// Checksum mismatches.
539 pub corruption_errs: u64,
540 /// Generation mismatches (stale data).
541 pub generation_errs: u64,
542}
543
544impl BtrfsDeviceStats {
545 /// Name for display: the device name, else `devid N`.
546 pub fn label(&self) -> String {
547 if self.name.is_empty() {
548 format!("devid {}", self.devid)
549 } else {
550 self.name.clone()
551 }
552 }
553
554 /// Sum of all error counters.
555 pub fn total_errors(&self) -> u64 {
556 self.write_errs
557 + self.read_errs
558 + self.flush_errs
559 + self.corruption_errs
560 + self.generation_errs
561 }
562}
563
564/// Space allocation of one btrfs block group type and RAID profile.
565#[derive(Debug, Clone, Default, Serialize, Deserialize)]
566#[serde(default)]
567pub struct BtrfsAllocation {
568 /// Block group type: `"data"`, `"metadata"` or `"system"`.
569 pub kind: String,
570 /// RAID profile (e.g. `"single"`, `"raid0"`, `"raid1c3"`).
571 pub profile: String,
572 /// Space allocated to this profile (logical size).
573 pub total_bytes: u64,
574 /// Space used within the allocation (logical size).
575 pub used_bytes: u64,
576}
577
578/// One Linux md array parsed from `/proc/mdstat`.
579#[derive(Debug, Clone, Default, Serialize, Deserialize)]
580#[serde(default)]
581pub struct MdArray {
582 /// Array name (e.g. `"md0"`).
583 pub name: String,
584 /// Whether the array is active.
585 pub active: bool,
586 /// Whether the array is read-only (`active (read-only)` / `(auto-read-only)`).
587 pub read_only: bool,
588 /// RAID level (e.g. `"raid1"`); empty for inactive arrays.
589 pub level: String,
590 /// Member devices.
591 pub members: Vec<MdMember>,
592 /// Number of devices the array should have (`[n/m]` -> n).
593 pub expected_devices: Option<u32>,
594 /// Number of devices currently in sync (`[n/m]` -> m).
595 pub working_devices: Option<u32>,
596 /// Ongoing resync/recovery/reshape/check, if any.
597 pub sync: Option<MdSync>,
598}
599
600impl MdArray {
601 /// Returns true if the array runs with fewer devices than expected or has a failed member.
602 pub fn is_degraded(&self) -> bool {
603 let short = match (self.expected_devices, self.working_devices) {
604 (Some(expected), Some(working)) => working < expected,
605 _ => false,
606 };
607 short || self.members.iter().any(|m| m.faulty)
608 }
609}
610
611/// A member device of an md array.
612#[derive(Debug, Clone, Default, Serialize, Deserialize)]
613#[serde(default)]
614pub struct MdMember {
615 /// Device name (e.g. `"sda1"`).
616 pub name: String,
617 /// Role number within the array.
618 pub role: u32,
619 /// Marked faulty `(F)`.
620 pub faulty: bool,
621 /// Spare device `(S)`.
622 pub spare: bool,
623}
624
625/// Progress of an md background operation.
626#[derive(Debug, Clone, Default, Serialize, Deserialize)]
627#[serde(default)]
628pub struct MdSync {
629 /// Operation name: `"resync"`, `"recovery"`, `"reshape"` or `"check"`.
630 pub action: String,
631 /// Completion percentage.
632 pub percent: f32,
633 /// Estimated minutes until completion.
634 pub finish_minutes: Option<f32>,
635 /// Current speed in KiB/s.
636 pub speed_kib_per_sec: Option<u64>,
637}
638
639/// Traffic of one network interface.
640#[derive(Debug, Clone, Default, Serialize, Deserialize)]
641#[serde(default)]
642pub struct NetworkStats {
643 /// Interface name (e.g. `"enp9s0"`).
644 pub name: String,
645 /// Receive throughput.
646 pub rx_bytes_per_sec: f64,
647 /// Transmit throughput.
648 pub tx_bytes_per_sec: f64,
649 /// Bytes received since the interface came up.
650 pub rx_total_bytes: u64,
651 /// Bytes transmitted since the interface came up.
652 pub tx_total_bytes: u64,
653 /// Negotiated link speed in Mbit/s (wired links), if known.
654 pub link_speed_mbps: Option<u32>,
655 /// Whether the link is up, if the OS reports it.
656 pub link_up: Option<bool>,
657 /// Wi-Fi link details, for wireless interfaces.
658 pub wireless: Option<WirelessLink>,
659}
660
661/// State of a Wi-Fi link.
662#[derive(Debug, Clone, Default, Serialize, Deserialize)]
663#[serde(default)]
664pub struct WirelessLink {
665 /// Current receive bit rate in Mbit/s.
666 pub rx_bitrate_mbps: Option<f32>,
667 /// Current transmit bit rate in Mbit/s.
668 pub tx_bitrate_mbps: Option<f32>,
669 /// Signal strength in dBm.
670 pub signal_dbm: Option<i32>,
671}
672
673/// One process.
674#[derive(Debug, Clone, Default, Serialize, Deserialize)]
675#[serde(default)]
676pub struct ProcessInfo {
677 /// Process id.
678 pub pid: u32,
679 /// Parent process id.
680 pub parent_pid: Option<u32>,
681 /// Short name (executable name).
682 pub name: String,
683 /// Full command line joined by spaces.
684 pub command: String,
685 /// Owner user name (or numeric uid when it cannot be resolved).
686 pub user: String,
687 /// Scheduling state (e.g. "Run", "Sleep", "Zombie").
688 pub state: String,
689 /// CPU usage; 100 means one full core, so multi-threaded processes may exceed 100.
690 pub cpu_percent: f32,
691 /// Resident memory.
692 pub memory_bytes: u64,
693 /// Number of threads, where known.
694 pub threads: Option<u32>,
695 /// Disk read throughput.
696 pub read_bytes_per_sec: f64,
697 /// Disk write throughput.
698 pub write_bytes_per_sec: f64,
699 /// Seconds since the process started.
700 pub run_time_secs: u64,
701 /// Kernel thread (e.g. `kworker`), which front-ends may hide.
702 pub kernel_thread: bool,
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn process(pid: u32, cpu: f32) -> ProcessInfo {
710 ProcessInfo {
711 pid,
712 cpu_percent: cpu,
713 ..Default::default()
714 }
715 }
716
717 #[test]
718 fn cpu_power_falls_back_to_a_laptop_apu_only() {
719 let power = |cpu: Option<f32>, igpu: Option<f32>| PowerStats {
720 cpu_package_w: cpu,
721 igpu_w: igpu,
722 ..Default::default()
723 };
724 assert_eq!(
725 power(Some(30.0), Some(6.0)).cpu_or_soc_w(),
726 ("CPU", Some(30.0))
727 );
728 assert_eq!(power(None, Some(6.0)).cpu_or_soc_w(), ("SoC", Some(6.0)));
729 // A desktop Ryzen iGPU's few milliwatts are not the SoC.
730 assert_eq!(power(None, Some(0.007)).cpu_or_soc_w(), ("CPU", None));
731 assert_eq!(power(None, None).cpu_or_soc_w(), ("CPU", None));
732 }
733
734 #[test]
735 fn truncate_keeps_top_cpu_processes_in_order() {
736 let mut snapshot = Snapshot {
737 processes: vec![
738 process(1, 5.0),
739 process(2, 50.0),
740 process(3, 0.0),
741 process(4, 20.0),
742 ],
743 process_count: 4,
744 ..Default::default()
745 };
746 snapshot.truncate_processes(2);
747 let pids: Vec<u32> = snapshot.processes.iter().map(|p| p.pid).collect();
748 assert_eq!(pids, vec![2, 4]);
749 // The total is preserved for display ("2 of 4 processes").
750 assert_eq!(snapshot.process_count, 4);
751 }
752
753 #[test]
754 fn truncate_leaves_out_kernel_threads() {
755 let kworker = |pid: u32, cpu: f32| ProcessInfo {
756 kernel_thread: true,
757 ..process(pid, cpu)
758 };
759 let mut snapshot = Snapshot {
760 processes: vec![
761 kworker(1, 9.0),
762 process(2, 5.0),
763 kworker(3, 8.0),
764 process(4, 1.0),
765 process(5, 0.0),
766 ],
767 process_count: 5,
768 ..Default::default()
769 };
770 snapshot.truncate_processes(3);
771 let pids: Vec<u32> = snapshot.processes.iter().map(|p| p.pid).collect();
772 assert_eq!(pids, vec![2, 4, 5], "busy kworkers take no places");
773 assert_eq!(snapshot.process_count, 5);
774 }
775
776 #[test]
777 fn snapshot_round_trips_through_json() {
778 let snapshot = Snapshot {
779 timestamp_ms: 42,
780 processes: vec![process(7, 1.5)],
781 ..Default::default()
782 };
783 let json = serde_json::to_string(&snapshot).unwrap();
784 let back: Snapshot = serde_json::from_str(&json).unwrap();
785 assert_eq!(back.timestamp_ms, 42);
786 assert_eq!(back.processes[0].pid, 7);
787 }
788
789 #[test]
790 fn device_matching_handles_nvme_namespaces() {
791 assert!(device_matches_disk("sda", "sda"));
792 assert!(device_matches_disk("nvme0", "nvme0n1"));
793 assert!(device_matches_disk("nvme1", "nvme1n1"));
794 assert!(!device_matches_disk("nvme1", "nvme10n1"));
795 assert!(!device_matches_disk("sd", "sda"));
796 assert!(!device_matches_disk("sda", "sdb"));
797 }
798
799 #[test]
800 fn smart_attention_flags() {
801 let healthy = SmartDevice {
802 passed: Some(true),
803 media_errors: Some(0),
804 available_spare_percent: Some(100),
805 available_spare_threshold_percent: Some(10),
806 ..Default::default()
807 };
808 assert!(!healthy.needs_attention());
809 let worn = SmartDevice {
810 available_spare_percent: Some(5),
811 ..healthy.clone()
812 };
813 assert!(worn.needs_attention());
814 let reallocated = SmartDevice {
815 reallocated_sectors: Some(8),
816 ..healthy
817 };
818 assert!(reallocated.needs_attention());
819 }
820
821 #[test]
822 fn md_array_degraded_detection() {
823 let mut array = MdArray {
824 expected_devices: Some(2),
825 working_devices: Some(2),
826 ..Default::default()
827 };
828 assert!(!array.is_degraded());
829 array.working_devices = Some(1);
830 assert!(array.is_degraded());
831 }
832}