diff --git a/src/cpu.rs b/src/cpu.rs index 3c0f332..7a60ee3 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -8,7 +8,6 @@ use crate::fs; #[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, - temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -172,7 +171,6 @@ impl Cpu { } self.rescan_stat(cache)?; - self.rescan_temperature(cache)?; Ok(()) } @@ -347,60 +345,6 @@ impl Cpu { 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/monitor.rs b/src/monitor.rs index d4534ba..40b0242 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -10,178 +10,30 @@ use std::{ time::SystemTime, }; -pub fn get_system_info() -> SystemInfo { - let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string()); +// Try /sys/devices/platform paths for thermal zones as a last resort +// if temperature_celsius.is_none() { +// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { +// for entry in thermal_zones.flatten() { +// let zone_path = entry.path(); +// let name = entry.file_name().into_string().unwrap_or_default(); - SystemInfo { cpu_model } -} - -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. - let mut temperature_celsius: Option = None; - - // Search for temperature in hwmon devices - if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") { - for hw_entry in hwmon_dir.flatten() { - let hw_path = hw_entry.path(); - - // Check hwmon driver name - if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) { - // Intel CPU temperature driver - if name == "coretemp" { - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - } - // AMD CPU temperature driver - // TODO: 'zenergy' can also report those stats, I think? - else if name == "k10temp" || name == "zenpower" || name == "amdgpu" { - // AMD's k10temp doesn't always label cores individually - // First try to find core-specific temps - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") { - temperature_celsius = Some(temp); - break; - } - - // Try Tctl temperature (CPU control temp) - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "Tctl") { - temperature_celsius = Some(temp); - break; - } - - // Try CPU temperature - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "CPU") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any available temperature input without a specific label - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - // Other CPU temperature drivers - else if name.contains("cpu") || name.contains("temp") { - // Try to find a label that matches this core - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any temperature reading if specific core not found - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - } - } - } - - // Try /sys/devices/platform paths for thermal zones as a last resort - if temperature_celsius.is_none() { - if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { - for entry in thermal_zones.flatten() { - let zone_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - if name.starts_with("thermal_zone") { - // Try to match by type - if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { - if zone_type.contains("cpu") - || zone_type.contains("x86") - || zone_type.contains("core") - { - if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { - temperature_celsius = Some(temp_mc as f32 / 1000.0); - break; - } - } - } - } - } - } - } - - Ok(CpuCoreInfo { - core_id, - temperature_celsius, - }) -} - -/// Finds core-specific temperature -fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option { - for i in 1..=32 { - // Increased range to handle systems with many sensors - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - // Match various common label formats: - // "Core X", "core X", "Core-X", "CPU Core X", etc. - let core_pattern = format!("{label_prefix} {core_id}"); - let alt_pattern = format!("{label_prefix}-{core_id}"); - - if label.eq_ignore_ascii_case(&core_pattern) - || label.eq_ignore_ascii_case(&alt_pattern) - || label - .to_lowercase() - .contains(&format!("core {core_id}").to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } - } - None -} - -// Finds generic sensor temperatures by label -fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option { - for i in 1..=32 { - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - if label.eq_ignore_ascii_case(label_name) - || label.to_lowercase().contains(&label_name.to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } else if !label_path.exists() && input_path.exists() { - // Some sensors might not have labels but still have valid temp inputs - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} - -// Fallback to any temperature reading from a sensor -fn get_fallback_temperature(hw_path: &Path) -> Option { - for i in 1..=32 { - let input_path = hw_path.join(format!("temp{i}_input")); - - if input_path.exists() { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} +// if name.starts_with("thermal_zone") { +// // Try to match by type +// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { +// if zone_type.contains("cpu") +// || zone_type.contains("x86") +// || zone_type.contains("core") +// { +// if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { +// temperature_celsius = Some(temp_mc as f32 / 1000.0); +// break; +// } +// } +// } +// } +// } +// } +// } pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); diff --git a/src/system.rs b/src/system.rs index 57d5ce2..4a86893 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, path::Path}; + use anyhow::{Context, bail}; use crate::{cpu, fs, power_supply}; @@ -10,6 +12,8 @@ pub struct System { pub load_average_15min: f64, pub cpus: Vec, + pub cpu_temperatures: HashMap, + pub power_supplies: Vec, } @@ -19,6 +23,8 @@ impl System { is_ac: false, cpus: Vec::new(), + cpu_temperatures: HashMap::new(), + power_supplies: Vec::new(), load_average_1min: 0.0, @@ -48,6 +54,110 @@ impl System { Ok(()) } + fn rescan_temperatures(&mut self) -> anyhow::Result<()> { + const PATH: &str = "/sys/class/hwmon"; + + let mut 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 { + // TODO: 'zenergy' can also report those stats, I think? + "coretemp" | "k10temp" | "zenpower" | "amdgpu" => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + // Other CPU temperature drivers. + _ if name.contains("cpu") || name.contains("temp") => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + _ => {} + } + } + + self.cpu_temperatures = temperatures; + + Ok(()) + } + + fn get_temperatures( + device_path: &Path, + temperatures: &mut HashMap, + ) -> anyhow::Result<()> { + // Increased range to handle systems with many sensors. + for i in 1..=96 { + let label_path = device_path.join(format!("temp{i}_label")); + let input_path = device_path.join(format!("temp{i}_input")); + + if !label_path.exists() || !input_path.exists() { + continue; + } + + let Some(label) = fs::read(&label_path).with_context(|| { + format!( + "failed to read hardware hardware device label from '{path}'", + path = label_path.display(), + ) + })? + else { + continue; + }; + + // Match various common label formats: + // "Core X", "core X", "Core-X", "CPU Core X", etc. + let number = label + .trim_start_matches("cpu") + .trim_start_matches("CPU") + .trim_start() + .trim_start_matches("core") + .trim_start_matches("Core") + .trim_start() + .trim_start_matches("tdie") + .trim_start_matches("Tdie") + .trim_start() + .trim_start_matches("tctl") + .trim_start_matches("Tctl") + .trim_start() + .trim_start_matches("-") + .trim(); + + let Ok(number) = number.parse::() else { + continue; + }; + + let Some(temperature_mc) = fs::read_n::(&input_path).with_context(|| { + format!( + "failed to read CPU temperature from '{path}'", + path = input_path.display(), + ) + })? + else { + continue; + }; + + temperatures.insert(number, temperature_mc as f64 / 1000.0); + } + + Ok(()) + } + fn is_desktop(&mut self) -> anyhow::Result { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?