use crate::platform::cpu_usage::{CpuTimeDelta, TotalsSampler};
use crate::sensor::{CpuTotals, ProcessCpuUtilization};
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
pub(crate) const MAX_KERNEL_FILE_BYTES: u64 = 1024 * 1024;
pub(crate) fn read_capped(path: &Path, max_bytes: u64) -> io::Result<String> {
let mut contents = String::new();
File::open(path)?
.take(max_bytes.saturating_add(1))
.read_to_string(&mut contents)?;
if contents.len() as u64 > max_bytes {
return Err(io::Error::other(format!(
"{} holds more than the {max_bytes} byte limit",
path.display()
)));
}
Ok(contents)
}
pub(crate) fn cpu_usage() -> TotalsSampler {
TotalsSampler::new(read_proc_stat)
}
pub(crate) fn process_tracker() -> Box<dyn ProcessCpuUtilization> {
Box::new(ProcfsProcessTracker::default())
}
#[derive(Debug, Default)]
struct ProcfsProcessTracker {
delta: CpuTimeDelta,
}
impl ProcessCpuUtilization for ProcfsProcessTracker {
fn process_cpu_utilization(&mut self, pid: u32, cpu_total: Option<u64>) -> f64 {
let Some(cpu_total) = cpu_total.or_else(system_cpu_total) else {
return 0.0;
};
let Some(process_time) = pid_cpu_time(pid) else {
return 0.0;
};
self.delta.share(cpu_total, process_time)
}
}
fn read_proc_stat() -> Option<CpuTotals> {
let raw = read_capped(Path::new("/proc/stat"), MAX_KERNEL_FILE_BYTES).ok()?;
parse_proc_stat(&raw)
}
fn system_cpu_total() -> Option<u64> {
read_proc_stat().map(|totals| totals.total)
}
fn parse_proc_stat(content: &str) -> Option<CpuTotals> {
let line = content.lines().next()?;
if !line.starts_with("cpu ") {
return None;
}
let fields: Vec<u64> = line
.split_whitespace()
.skip(1)
.map(|field| field.parse().unwrap_or(0))
.collect();
if fields.len() < 8 {
return None;
}
let total = fields
.iter()
.take(8)
.try_fold(0u64, |a, &b| a.checked_add(b))?;
let idle = fields[3] + fields[4];
Some(CpuTotals::new(total, idle))
}
fn pid_cpu_time(pid: u32) -> Option<u64> {
let path = PathBuf::from(format!("/proc/{pid}/stat"));
let content = read_capped(&path, MAX_KERNEL_FILE_BYTES).ok()?;
let after_comm = &content[content.rfind(')')? + 1..];
let fields: Vec<&str> = after_comm.split_whitespace().collect();
let utime: u64 = fields.get(11)?.parse().ok()?;
let stime: u64 = fields.get(12)?.parse().ok()?;
Some(utime + stime)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
const SAMPLE: &str = "cpu 100 20 30 400 50 6 7 8 0 0\ncpu0 1 2 3 4 5 6 7 8 0 0\n";
#[test]
fn parses_the_aggregate_cpu_line() {
let totals = parse_proc_stat(SAMPLE).unwrap();
assert_eq!(totals.total, 100 + 20 + 30 + 400 + 50 + 6 + 7 + 8);
assert_eq!(totals.idle, 400 + 50);
}
#[test]
fn guest_columns_beyond_the_first_eight_are_ignored() {
let with_guest = "cpu 100 20 30 400 50 6 7 8 900 900\n";
assert_eq!(parse_proc_stat(with_guest).unwrap().total, 621);
}
#[test]
fn rejects_input_that_is_not_proc_stat() {
assert!(parse_proc_stat("").is_none());
assert!(parse_proc_stat("intr 1 2 3\n").is_none());
assert!(parse_proc_stat("cpu 1 2 3\n").is_none());
}
#[test]
fn unparsable_columns_count_as_zero_rather_than_failing() {
let totals = parse_proc_stat("cpu 100 x 30 400 50 6 7 8\n").unwrap();
assert_eq!(totals.total, 601);
}
fn file_holding(contents: &str) -> tempfile::NamedTempFile {
let mut file = tempfile::NamedTempFile::new().expect("temp file");
file.write_all(contents.as_bytes()).expect("write");
file.flush().expect("flush");
file
}
#[test]
fn reads_a_file_that_fits() {
let file = file_holding("12345\n");
assert_eq!(read_capped(file.path(), 64).unwrap(), "12345\n");
}
#[test]
fn a_file_exactly_at_the_limit_still_reads() {
let file = file_holding("abcd");
assert_eq!(read_capped(file.path(), 4).unwrap(), "abcd");
}
#[test]
fn an_oversized_file_is_an_error_rather_than_a_truncated_read() {
let file = file_holding("123456");
let error = read_capped(file.path(), 3).unwrap_err();
assert!(
error.to_string().contains("more than the 3 byte limit"),
"unexpected message: {error}"
);
}
#[test]
fn a_missing_path_keeps_its_io_error_kind() {
let error = read_capped(Path::new("/proc/definitely-not-here"), 64).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}
#[test]
fn a_process_that_does_not_exist_reports_nothing() {
assert_eq!(pid_cpu_time(0), None);
}
}