mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
system: cpu temperatures scanning
This commit is contained in:
parent
421d4aaacc
commit
303a84479c
3 changed files with 133 additions and 227 deletions
56
src/cpu.rs
56
src/cpu.rs
|
@ -8,7 +8,6 @@ use crate::fs;
|
||||||
#[derive(Default, Debug, Clone, PartialEq)]
|
#[derive(Default, Debug, Clone, PartialEq)]
|
||||||
pub struct CpuRescanCache {
|
pub struct CpuRescanCache {
|
||||||
stat: OnceCell<HashMap<u32, CpuStat>>,
|
stat: OnceCell<HashMap<u32, CpuStat>>,
|
||||||
temperatures: OnceCell<HashMap<u32, f64>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -172,7 +171,6 @@ impl Cpu {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.rescan_stat(cache)?;
|
self.rescan_stat(cache)?;
|
||||||
self.rescan_temperature(cache)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -347,60 +345,6 @@ impl Cpu {
|
||||||
Ok(())
|
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<()> {
|
pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> {
|
||||||
let Self {
|
let Self {
|
||||||
number,
|
number,
|
||||||
|
|
194
src/monitor.rs
194
src/monitor.rs
|
@ -10,178 +10,30 @@ use std::{
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn get_system_info() -> SystemInfo {
|
// Try /sys/devices/platform paths for thermal zones as a last resort
|
||||||
let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string());
|
// 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 }
|
// if name.starts_with("thermal_zone") {
|
||||||
}
|
// // Try to match by type
|
||||||
|
// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) {
|
||||||
pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result<CpuCoreInfo> {
|
// if zone_type.contains("cpu")
|
||||||
// Temperature detection.
|
// || zone_type.contains("x86")
|
||||||
// Should be generic enough to be able to support for multiple hardware sensors
|
// || zone_type.contains("core")
|
||||||
// with the possibility of extending later down the road.
|
// {
|
||||||
let mut temperature_celsius: Option<f32> = None;
|
// if let Ok(temp_mc) = read_sysfs_value::<i32>(zone_path.join("temp")) {
|
||||||
|
// temperature_celsius = Some(temp_mc as f32 / 1000.0);
|
||||||
// Search for temperature in hwmon devices
|
// break;
|
||||||
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::<i32>(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<f32> {
|
|
||||||
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::<i32>(&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<f32> {
|
|
||||||
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::<i32>(&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::<i32>(&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<f32> {
|
|
||||||
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::<i32>(&input_path) {
|
|
||||||
return Some(temp_mc as f32 / 1000.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cpu_model() -> anyhow::Result<String> {
|
pub fn get_cpu_model() -> anyhow::Result<String> {
|
||||||
let path = Path::new("/proc/cpuinfo");
|
let path = Path::new("/proc/cpuinfo");
|
||||||
|
|
110
src/system.rs
110
src/system.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
use anyhow::{Context, bail};
|
||||||
|
|
||||||
use crate::{cpu, fs, power_supply};
|
use crate::{cpu, fs, power_supply};
|
||||||
|
@ -10,6 +12,8 @@ pub struct System {
|
||||||
pub load_average_15min: f64,
|
pub load_average_15min: f64,
|
||||||
|
|
||||||
pub cpus: Vec<cpu::Cpu>,
|
pub cpus: Vec<cpu::Cpu>,
|
||||||
|
pub cpu_temperatures: HashMap<u32, f64>,
|
||||||
|
|
||||||
pub power_supplies: Vec<power_supply::PowerSupply>,
|
pub power_supplies: Vec<power_supply::PowerSupply>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +23,8 @@ impl System {
|
||||||
is_ac: false,
|
is_ac: false,
|
||||||
|
|
||||||
cpus: Vec::new(),
|
cpus: Vec::new(),
|
||||||
|
cpu_temperatures: HashMap::new(),
|
||||||
|
|
||||||
power_supplies: Vec::new(),
|
power_supplies: Vec::new(),
|
||||||
|
|
||||||
load_average_1min: 0.0,
|
load_average_1min: 0.0,
|
||||||
|
@ -48,6 +54,110 @@ impl System {
|
||||||
Ok(())
|
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<u32, f64>,
|
||||||
|
) -> 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::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(temperature_mc) = fs::read_n::<i64>(&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<bool> {
|
fn is_desktop(&mut self) -> anyhow::Result<bool> {
|
||||||
if let Some(chassis_type) =
|
if let Some(chassis_type) =
|
||||||
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?
|
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")?
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue