use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::Result;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use super::TempSensorData;
#[cfg(feature = "gpu")]
use crate::collection::amd::get_amd_name;
use crate::{app::filter::Filter, collection::linux::utils::is_device_awake};
const EMPTY_NAME: &str = "Unknown";
struct HwmonResults {
temperatures: Vec<TempSensorData>,
num_hwmon: usize,
}
fn parse_temp(path: &Path) -> Result<f32> {
Ok(fs::read_to_string(path)?.trim_end().parse::<f32>()? / 1_000.0)
}
fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
let mut dirs = HashSet::default();
if let Ok(read_dir) = Path::new("/sys/class/hwmon").read_dir() {
for entry in read_dir.flatten() {
let mut path = entry.path();
if !path.join("temp1_input").exists() {
if path.join("device/temp1_input").exists() {
path.push("device");
}
}
dirs.insert(path);
}
}
let num_hwmon = dirs.len();
if let Ok(read_dir) = Path::new("/sys/devices/platform").read_dir() {
for entry in read_dir.flatten() {
if entry.file_name().to_string_lossy().starts_with("coretemp.") {
if let Ok(read_dir) = entry.path().join("hwmon").read_dir() {
for entry in read_dir.flatten() {
let path = entry.path();
if path.join("temp1_input").exists() {
if let Some(child) = path.file_name() {
let to_check_path = Path::new("/sys/class/hwmon").join(child);
if !dirs.contains(&to_check_path) {
dirs.insert(path);
}
}
}
}
}
}
}
}
(dirs, num_hwmon)
}
#[inline]
fn read_to_string_lossy<P: AsRef<Path>>(path: P) -> Option<String> {
fs::read(path)
.map(|v| String::from_utf8_lossy(&v).trim().to_string())
.ok()
}
#[inline]
fn humanize_name(name: String, sensor_name: Option<&String>) -> String {
match sensor_name {
Some(ty) => format!("{name} ({ty})"),
None => name,
}
}
#[inline]
fn counted_name(seen_names: &mut HashMap<String, u32>, name: String) -> String {
if let Some(count) = seen_names.get_mut(&name) {
*count += 1;
format!("{name} ({count})")
} else {
seen_names.insert(name.clone(), 0);
name
}
}
fn uppercase_first_letter(s: &mut str) {
if let Some(r) = s.get_mut(0..1) {
r.make_ascii_uppercase();
}
}
fn finalize_name(
hwmon_name: Option<String>, sensor_label: Option<String>,
fallback_sensor_name: &Option<String>, seen_names: &mut HashMap<String, u32>,
) -> String {
let candidate_name = match (hwmon_name, sensor_label) {
(Some(name), Some(mut label)) => match (name.is_empty(), label.is_empty()) {
(false, false) => {
uppercase_first_letter(&mut label);
format!("{name}: {label}")
}
(true, false) => {
uppercase_first_letter(&mut label);
match fallback_sensor_name {
Some(fallback) if !fallback.is_empty() => {
format!("{fallback}: {label}")
}
_ => label,
}
}
(false, true) => name.to_owned(),
(true, true) => EMPTY_NAME.to_string(),
},
(None, Some(mut label)) => match fallback_sensor_name {
Some(fallback) if !fallback.is_empty() => {
if label.is_empty() {
fallback.to_owned()
} else {
uppercase_first_letter(&mut label);
format!("{fallback}: {label}")
}
}
_ => {
if label.is_empty() {
EMPTY_NAME.to_string()
} else {
uppercase_first_letter(&mut label);
label
}
}
},
(Some(name), None) => {
if name.is_empty() {
EMPTY_NAME.to_string()
} else {
name
}
}
(None, None) => match fallback_sensor_name {
Some(sensor_name) if !sensor_name.is_empty() => sensor_name.to_owned(),
_ => EMPTY_NAME.to_string(),
},
};
counted_name(seen_names, candidate_name)
}
fn hwmon_temperatures(filter: &Option<Filter>) -> HwmonResults {
let mut temperatures: Vec<TempSensorData> = vec![];
let mut seen_names: HashMap<String, u32> = HashMap::default();
let (dirs, num_hwmon) = get_hwmon_candidates();
for file_path in dirs {
let sensor_name = read_to_string_lossy(file_path.join("name"));
let device = file_path.join("device");
if !is_device_awake(&device) {
let name = finalize_name(None, None, &sensor_name, &mut seen_names);
temperatures.push(TempSensorData {
name,
temperature: None,
});
continue;
}
if let Ok(dir_entries) = file_path.read_dir() {
for file in dir_entries.flatten() {
let name = file.file_name();
let name = name.to_string_lossy();
if !(name.starts_with("temp") && name.ends_with("input")) {
continue;
}
let temp_path = file.path();
let sensor_label_path = file_path.join(name.replace("input", "label"));
let sensor_label = read_to_string_lossy(sensor_label_path);
let hwmon_name = {
let device = file_path.join("device");
let drm = device.join("drm");
if drm.exists() {
#[cfg(feature = "gpu")]
{
if let Some(amd_gpu_name) = get_amd_name(&device) {
Some(amd_gpu_name)
} else if let Ok(cards) = drm.read_dir() {
cards.flatten().find_map(|card| {
card.file_name().to_str().and_then(|name| {
name.starts_with("card").then(|| {
humanize_name(
name.trim().to_string(),
sensor_name.as_ref(),
)
})
})
})
} else {
None
}
}
#[cfg(not(feature = "gpu"))]
{
if let Ok(cards) = drm.read_dir() {
cards.flatten().find_map(|card| {
card.file_name().to_str().and_then(|name| {
name.starts_with("card").then(|| {
humanize_name(
name.trim().to_string(),
sensor_name.as_ref(),
)
})
})
})
} else {
None
}
}
} else {
fs::read_link(device).ok().and_then(|link| {
let link = link
.file_name()
.and_then(|f| f.to_str())
.map(|s| s.trim().to_owned());
match link {
Some(link) if link.as_bytes()[0].is_ascii_alphabetic() => {
Some(humanize_name(link, sensor_name.as_ref()))
}
_ => None,
}
})
}
};
let name = finalize_name(hwmon_name, sensor_label, &sensor_name, &mut seen_names);
if Filter::optional_should_keep(filter, &name) {
if let Ok(temp_celsius) = parse_temp(&temp_path) {
temperatures.push(TempSensorData {
name,
temperature: Some(temp_celsius),
});
}
}
}
}
}
HwmonResults {
temperatures,
num_hwmon,
}
}
fn add_thermal_zone_temperatures(temperatures: &mut Vec<TempSensorData>, filter: &Option<Filter>) {
let path = Path::new("/sys/class/thermal");
let Ok(read_dir) = path.read_dir() else {
return;
};
let mut seen_names: HashMap<String, u32> = HashMap::default();
for entry in read_dir.flatten() {
if entry
.file_name()
.to_string_lossy()
.starts_with("thermal_zone")
{
let file_path = entry.path();
let name_path = file_path.join("type");
if let Some(name) = read_to_string_lossy(name_path) {
let name = if name.is_empty() {
EMPTY_NAME.to_string()
} else {
name
};
if Filter::optional_should_keep(filter, &name) {
let temp_path = file_path.join("temp");
if let Ok(temp_celsius) = parse_temp(&temp_path) {
let name = counted_name(&mut seen_names, name);
temperatures.push(TempSensorData {
name,
temperature: Some(temp_celsius),
});
}
}
}
}
}
}
pub fn get_temperature_data(filter: &Option<Filter>) -> Result<Option<Vec<TempSensorData>>> {
let mut results = hwmon_temperatures(filter);
if results.num_hwmon == 0 {
add_thermal_zone_temperatures(&mut results.temperatures, filter);
}
Ok(Some(results.temperatures))
}
#[cfg(test)]
mod tests {
use rustc_hash::FxHashMap as HashMap;
use super::finalize_name;
#[test]
fn test_finalize_name() {
let mut seen_names = HashMap::default();
assert_eq!(
finalize_name(
Some("hwmon".to_string()),
Some("sensor".to_string()),
&Some("test".to_string()),
&mut seen_names
),
"hwmon: Sensor"
);
assert_eq!(
finalize_name(
Some("hwmon".to_string()),
None,
&Some("test".to_string()),
&mut seen_names
),
"hwmon"
);
assert_eq!(
finalize_name(
None,
Some("sensor".to_string()),
&Some("test".to_string()),
&mut seen_names
),
"test: Sensor"
);
assert_eq!(
finalize_name(
Some("hwmon".to_string()),
Some("sensor".to_string()),
&Some("test".to_string()),
&mut seen_names
),
"hwmon: Sensor (1)"
);
assert_eq!(
finalize_name(None, None, &Some("test".to_string()), &mut seen_names),
"test"
);
assert_eq!(finalize_name(None, None, &None, &mut seen_names), "Unknown");
assert_eq!(
finalize_name(None, None, &Some("test".to_string()), &mut seen_names),
"test (1)"
);
assert_eq!(
finalize_name(None, None, &None, &mut seen_names),
"Unknown (1)"
);
assert_eq!(
finalize_name(Some(String::default()), None, &None, &mut seen_names),
"Unknown (2)"
);
assert_eq!(
finalize_name(None, Some(String::default()), &None, &mut seen_names),
"Unknown (3)"
);
assert_eq!(
finalize_name(None, None, &Some(String::default()), &mut seen_names),
"Unknown (4)"
);
}
}