use super::{DiskInfo, ProcessInfo};
pub fn parse_cpu(current: &str, previous: &str) -> (f64, Vec<f64>) {
let curr_cpus = parse_cpu_lines(current);
let prev_cpus = parse_cpu_lines(previous);
let mut per_core = Vec::new();
let mut aggregate = 0.0;
for (i, (curr, prev)) in curr_cpus.iter().zip(prev_cpus.iter()).enumerate() {
let idle_delta = curr.idle.saturating_sub(prev.idle) as f64;
let total_delta = curr.total.saturating_sub(prev.total) as f64;
if total_delta > 0.0 {
let usage = (1.0 - idle_delta / total_delta) * 100.0;
if i == 0 {
aggregate = usage;
} else {
per_core.push(usage);
}
}
}
(aggregate, per_core)
}
struct CpuTimes {
idle: u64,
total: u64,
}
fn parse_cpu_lines(raw: &str) -> Vec<CpuTimes> {
let mut result = Vec::new();
for line in raw.lines() {
if !line.starts_with("cpu") {
continue;
}
let parts: Vec<u64> = line
.split_whitespace()
.skip(1) .filter_map(|s| s.parse().ok())
.collect();
if parts.len() >= 4 {
let idle = parts[3] + parts.get(4).copied().unwrap_or(0); let total: u64 = parts.iter().sum();
result.push(CpuTimes { idle, total });
}
}
result
}
pub fn parse_meminfo(raw: &str) -> (u64, u64, u64, u64, u64) {
let mut total = 0u64;
let mut free = 0u64;
let mut available = 0u64;
let mut buffers = 0u64;
let mut cached = 0u64;
let mut swap_total = 0u64;
let mut swap_free = 0u64;
for line in raw.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let val: u64 = parts[1].parse().unwrap_or(0);
match parts[0] {
"MemTotal:" => total = val,
"MemFree:" => free = val,
"MemAvailable:" => available = val,
"Buffers:" => buffers = val,
"Cached:" => cached = val,
"SwapTotal:" => swap_total = val,
"SwapFree:" => swap_free = val,
_ => {}
}
}
if available == 0 {
available = free + buffers + cached;
}
let used = total.saturating_sub(available);
let swap_used = swap_total.saturating_sub(swap_free);
(total, used, available, swap_total, swap_used)
}
pub fn parse_loadavg(raw: &str) -> (f64, f64, f64) {
let parts: Vec<&str> = raw.split_whitespace().collect();
let l1 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0.0);
let l5 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0.0);
let l15 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0.0);
(l1, l5, l15)
}
pub fn parse_df(raw: &str) -> Vec<DiskInfo> {
let mut disks = Vec::new();
for line in raw.lines().skip(1) {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 6 {
continue;
}
let cap_idx = match parts
.iter()
.rposition(|p| p.ends_with('%') && p.trim_end_matches('%').parse::<u64>().is_ok())
{
Some(i) if i >= 3 => i, _ => continue,
};
let avail_idx = cap_idx - 1;
let used_idx = cap_idx - 2;
let blocks_idx = cap_idx - 3;
let fs_name: String = parts[..blocks_idx].join(" ");
let mount = if cap_idx + 1 < parts.len() {
parts[cap_idx + 1..].join(" ")
} else {
continue;
};
if fs_name == "none"
|| fs_name == "udev"
|| fs_name == "overlay"
|| fs_name == "devfs"
|| fs_name.starts_with("map ")
{
continue;
}
let total: u64 = parts[blocks_idx].parse().unwrap_or(0) * 1024;
let used: u64 = parts[used_idx].parse().unwrap_or(0) * 1024;
let _avail: u64 = parts[avail_idx].parse().unwrap_or(0) * 1024;
let use_pct_str = parts[cap_idx].trim_end_matches('%');
let use_pct: f64 = use_pct_str.parse().unwrap_or(0.0);
if total == 0 {
continue;
}
disks.push(DiskInfo {
mount,
total_bytes: total,
used_bytes: used,
use_pct,
});
}
disks
}
pub fn parse_ps(raw: &str, limit: usize) -> Vec<ProcessInfo> {
let mut procs = Vec::new();
for line in raw.lines().skip(1) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 11 {
continue;
}
let pid: u32 = parts[1].parse().unwrap_or(0);
let cpu_pct: f64 = parts[2].parse().unwrap_or(0.0);
let mem_pct: f64 = parts[3].parse().unwrap_or(0.0);
let rss: u64 = parts[5].parse().unwrap_or(0); let state = parts[7].to_string();
let name = parts[10..].join(" ");
if name.starts_with('[') {
continue;
}
procs.push(ProcessInfo {
pid,
name,
cpu_pct,
mem_pct,
mem_rss_kb: rss,
state,
});
if procs.len() >= limit {
break;
}
}
procs
}
pub fn parse_uptime(raw: &str) -> u64 {
raw.split_whitespace()
.next()
.and_then(|s| s.parse::<f64>().ok())
.map(|f| f as u64)
.unwrap_or(0)
}
pub fn parse_net_dev(current: &str, previous: &str, elapsed_secs: f64) -> (f64, f64) {
let curr = sum_net_bytes(current);
let prev = sum_net_bytes(previous);
let rx_delta = curr.0.saturating_sub(prev.0) as f64;
let tx_delta = curr.1.saturating_sub(prev.1) as f64;
if elapsed_secs > 0.0 {
(rx_delta / elapsed_secs, tx_delta / elapsed_secs)
} else {
(0.0, 0.0)
}
}
fn sum_net_bytes(raw: &str) -> (u64, u64) {
let mut rx_total = 0u64;
let mut tx_total = 0u64;
for line in raw.lines() {
let line = line.trim();
if !line.contains(':') || line.starts_with("Inter") || line.starts_with("face") {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 10 {
continue;
}
let iface = parts[0].trim_end_matches(':');
if iface == "lo" {
continue;
} let rx: u64 = parts[1].parse().unwrap_or(0);
let tx: u64 = parts[9].parse().unwrap_or(0);
rx_total += rx;
tx_total += tx;
}
(rx_total, tx_total)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_meminfo() {
let raw = "MemTotal: 16384000 kB\n\
MemFree: 2048000 kB\n\
MemAvailable: 8192000 kB\n\
Buffers: 512000 kB\n\
Cached: 4096000 kB\n\
SwapTotal: 4096000 kB\n\
SwapFree: 3072000 kB\n";
let (total, used, avail, swap_total, swap_used) = parse_meminfo(raw);
assert_eq!(total, 16384000);
assert_eq!(avail, 8192000);
assert_eq!(used, 16384000 - 8192000);
assert_eq!(swap_total, 4096000);
assert_eq!(swap_used, 1024000);
}
#[test]
fn test_parse_loadavg() {
let (l1, l5, l15) = parse_loadavg("0.42 0.38 0.35 1/234 5678\n");
assert!((l1 - 0.42).abs() < 0.001);
assert!((l5 - 0.38).abs() < 0.001);
assert!((l15 - 0.35).abs() < 0.001);
}
#[test]
fn test_parse_uptime() {
assert_eq!(parse_uptime("3641234.56 7282469.12\n"), 3641234);
}
#[test]
fn test_parse_df() {
let raw = "Filesystem 1024-blocks Used Available Capacity Mounted on\n\
/dev/sda1 102400000 24576000 77824000 24% /\n\
tmpfs 8192000 0 8192000 0% /dev/shm\n\
none 8192000 0 8192000 0% /run/lock\n\
/dev/sdb1 512000000 419430400 92569600 82% /data\n";
let disks = parse_df(raw);
assert_eq!(disks.len(), 3); assert_eq!(disks[0].mount, "/");
assert!((disks[0].use_pct - 24.0).abs() < 0.1);
assert_eq!(disks[1].mount, "/dev/shm");
assert_eq!(disks[2].mount, "/data");
}
#[test]
fn test_parse_df_macos_spaces() {
let raw = "Filesystem 512-blocks Used Available Capacity Mounted on\n\
/dev/disk3s1s1 478724992 24017264 256100400 9% /\n\
devfs 405 405 0 100% /dev\n\
/dev/disk3s5 478724992 179601016 256100400 42% /System/Volumes/Data\n\
map auto_home 0 0 0 100% /System/Volumes/Data/home\n\
/Users/mattbot/Downloads/Geekbench 6.app 478724992 179130792 256570640 42% /private/var/folders/6c/T/AppTranslocation/foo\n";
let disks = parse_df(raw);
assert_eq!(disks.len(), 3);
assert_eq!(disks[0].mount, "/");
assert!((disks[0].use_pct - 9.0).abs() < 0.1);
assert_eq!(disks[1].mount, "/System/Volumes/Data");
assert!((disks[1].use_pct - 42.0).abs() < 0.1);
assert_eq!(
disks[2].mount,
"/private/var/folders/6c/T/AppTranslocation/foo"
);
assert!((disks[2].use_pct - 42.0).abs() < 0.1);
}
#[test]
fn test_parse_ps() {
let raw = "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\n\
postgres 1842 28.3 12.1 500000 196608 ? Ss Jan01 100:00 /usr/lib/postgresql/14/bin/postgres\n\
node 2103 14.7 8.4 400000 134217 ? Sl Jan01 50:00 node /app/server.js\n\
root 891 3.2 0.8 50000 12800 ? Ss Jan01 10:00 nginx: master process\n";
let procs = parse_ps(raw, 10);
assert_eq!(procs.len(), 3);
assert_eq!(procs[0].pid, 1842);
assert!((procs[0].cpu_pct - 28.3).abs() < 0.1);
assert!(procs[0].name.contains("postgres"));
}
}