diff --git a/src/cpu.rs b/src/cpu.rs index 4979b67..3c0f332 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,9 +5,10 @@ use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; use crate::fs; -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, + temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -22,7 +23,28 @@ pub struct CpuStat { pub steal: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +impl CpuStat { + pub fn total(&self) -> u64 { + self.user + + self.nice + + self.system + + self.idle + + self.iowait + + self.irq + + self.softirq + + self.steal + } + + pub fn idle(&self) -> u64 { + self.idle + self.iowait + } + + pub fn usage(&self) -> f64 { + 1.0 - self.idle() as f64 / self.total() as f64 + } +} + +#[derive(Debug, Clone, PartialEq)] pub struct Cpu { pub number: u32, @@ -42,27 +64,8 @@ pub struct Cpu { pub epb: Option, pub stat: CpuStat, -} -impl Cpu { - pub fn time_total(&self) -> u64 { - self.stat.user - + self.stat.nice - + self.stat.system - + self.stat.idle - + self.stat.iowait - + self.stat.irq - + self.stat.softirq - + self.stat.steal - } - - pub fn time_idle(&self) -> u64 { - self.stat.idle + self.stat.iowait - } - - pub fn usage(&self) -> f64 { - 1.0 - self.time_idle() as f64 / self.time_total() as f64 - } + pub temperature: Option, } impl fmt::Display for Cpu { @@ -102,6 +105,8 @@ impl Cpu { softirq: 0, steal: 0, }, + + temperature: None, }; cpu.rescan(cache)?; @@ -116,9 +121,11 @@ impl Cpu { let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read contents of '{PATH}'"))? - .flatten() + .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + let entry_file_name = entry.file_name(); let Some(name) = entry_file_name.to_str() else { @@ -157,8 +164,6 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.rescan_stat(cache)?; - if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; @@ -166,50 +171,8 @@ impl Cpu { self.rescan_epb()?; } - Ok(()) - } - - fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let stat = match cache.stat.get() { - Some(stat) => stat, - - None => { - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; - - cache - .stat - .set(HashMap::from_iter(content.lines().skip(1).filter_map( - |line| { - let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - - let number = parts.next()?.parse().ok()?; - - let stat = CpuStat { - user: parts.next()?.parse().ok()?, - nice: parts.next()?.parse().ok()?, - system: parts.next()?.parse().ok()?, - idle: parts.next()?.parse().ok()?, - iowait: parts.next()?.parse().ok()?, - irq: parts.next()?.parse().ok()?, - softirq: parts.next()?.parse().ok()?, - steal: parts.next()?.parse().ok()?, - }; - - Some((number, stat)) - }, - ))); - - cache.stat.get().unwrap() - } - }; - - self.stat = stat - .get(&self.number) - .with_context(|| format!("failed to get stat of {self}"))? - .clone(); + self.rescan_stat(cache)?; + self.rescan_temperature(cache)?; Ok(()) } @@ -338,6 +301,106 @@ impl Cpu { Ok(()) } + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, + + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; + + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); + + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))) + .unwrap(); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); + + Ok(()) + } + + fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let temperatures = match cache.temperatures.get() { + Some(temperature) => temperature, + + None => { + const PATH: &str = "/sys/class/hwmon"; + + let temperatures = HashMap::new(); + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = + entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; + + match &*name { + // Intel CPU temperature driver + "coretemp" => todo!(), + + // AMD CPU temperature driver + // TODO: 'zenergy' can also report those stats, I think? + "k10temp" | "zenpower" | "amdgpu" => todo!(), + + // Other CPU temperature drivers + _ if name.contains("cpu") || name.contains("temp") => todo!(), + + _ => {} + } + } + + cache.temperatures.set(temperatures).unwrap(); + cache.temperatures.get().unwrap() + } + }; + + self.temperature = temperatures.get(&self.number).copied(); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/fs.rs b/src/fs.rs index 526856d..3192e4d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -8,11 +8,19 @@ pub fn exists(path: impl AsRef) -> bool { path.exists() } -pub fn read_dir(path: impl AsRef) -> anyhow::Result { +pub fn read_dir(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); - fs::read_dir(path) - .with_context(|| format!("failed to read directory '{path}'", path = path.display())) + match fs::read_dir(path) { + Ok(entries) => Ok(Some(entries)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => Err(error).context(format!( + "failed to read directory '{path}'", + path = path.display() + )), + } } pub fn read(path: impl AsRef) -> anyhow::Result> { @@ -23,9 +31,7 @@ pub fn read(path: impl AsRef) -> anyhow::Result> { Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), } } diff --git a/src/monitor.rs b/src/monitor.rs index 88195c8..d4534ba 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -16,11 +16,7 @@ pub fn get_system_info() -> SystemInfo { SystemInfo { cpu_model } } -pub fn get_cpu_core_info( - core_id: u32, - prev_times: &CpuTimes, - current_times: &CpuTimes, -) -> anyhow::Result { +pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -187,65 +183,6 @@ fn get_fallback_temperature(hw_path: &Path) -> Option { None } -pub fn get_all_cpu_core_info() -> anyhow::Result> { - let initial_cpu_times = read_all_cpu_times()?; - thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation - let final_cpu_times = read_all_cpu_times()?; - - let num_cores = get_real_cpus() - .map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?; - - let mut core_infos = Vec::with_capacity(num_cores as usize); - - for core_id in 0..num_cores { - if let (Some(prev), Some(curr)) = ( - initial_cpu_times.get(&core_id), - final_cpu_times.get(&core_id), - ) { - match get_cpu_core_info(core_id, prev, curr) { - Ok(info) => core_infos.push(info), - Err(e) => { - // Log or handle error for a single core, maybe push a partial info or skip - eprintln!("Error getting info for core {core_id}: {e}"); - } - } - } else { - // Log or handle missing times for a core - eprintln!("Missing CPU time data for core {core_id}"); - } - } - Ok(core_infos) -} - -pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - // Calculate average CPU temperature from the core temperatures - let average_temperature_celsius = if cpu_cores.is_empty() { - None - } else { - // Filter cores with temperature readings, then calculate average - let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores - .iter() - .filter(|core| core.temperature_celsius.is_some()) - .collect(); - - if cores_with_temp.is_empty() { - None - } else { - // Sum up all temperatures and divide by count - let sum: f32 = cores_with_temp - .iter() - .map(|core| core.temperature_celsius.unwrap()) - .sum(); - Some(sum / cores_with_temp.len() as f32) - } - }; - - // Return the constructed CpuGlobalInfo - CpuGlobalInfo { - average_temperature_celsius, - } -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index f213e5b..2155c29 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -155,6 +155,7 @@ impl PowerSupply { for entry in fs::read_dir(POWER_SUPPLY_PATH) .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + .with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? { let entry = match entry { Ok(entry) => entry, diff --git a/src/system.rs b/src/system.rs index 02b7b80..57d5ce2 100644 --- a/src/system.rs +++ b/src/system.rs @@ -99,8 +99,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("failed to read load average")? - .context("load average file doesn't exist, are you on linux?")?; + .context("failed to read load average from '/proc/loadavg'")? + .context("'/proc/loadavg' doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace();