diff --git a/examples/simple.rs b/examples/simple.rs index 7a0dd2d23..67646d1df 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -42,6 +42,7 @@ pid : displays this example's PID brand : displays CPU brand cpus : displays CPUs state frequency : displays CPU frequency +cpu_temperature : displays CPU temperature vendor_id : displays CPU vendor id = GPU commands = @@ -174,6 +175,10 @@ fn interpret_input( Some(usage) => println!(" usage: {usage}%"), None => println!(" usage: N/A"), } + match gpu.temperature() { + Some(temp) => println!(" temperature: {temp}C"), + None => println!(" temperature: N/A"), + } if let (Some(used), Some(total)) = (gpu.used_memory(), gpu.total_memory()) { println!(" memory: {}/{} KB", used / 1_000, total / 1_000); } else { @@ -226,6 +231,23 @@ fn interpret_input( println!("System information cannot be retrieved: {error}"); } }, + "cpu_temperature" => match sys { + Ok(sys) => { + for cpu in sys.cpus() { + print!("[{}] {}C", cpu.name(), cpu.temperature()); + if let Some(max) = cpu.max() { + print!(" (max: {max}C)"); + } + if let Some(critical) = cpu.critical() { + print!(" (critical: {critical}C)"); + } + println!(); + } + } + Err(error) => { + println!("System information cannot be retrieved: {error}"); + } + }, "vendor_id" => match sys { Ok(sys) => { println!("vendor ID: {}", sys.cpus()[0].vendor_id()); @@ -375,6 +397,16 @@ fn interpret_input( Ok(disks) => { for disk in disks { println!("{disk:?}"); + if let Some(temp) = disk.temperature() { + print!(" temperature: {temp}C"); + if let Some(max) = disk.max() { + print!(" (max: {max}C)"); + } + if let Some(critical) = disk.critical() { + print!(" (critical: {critical}C)"); + } + println!(); + } } } Err(error) => { @@ -484,7 +516,17 @@ fn interpret_input( ); } "motherboard" => match Motherboard::new() { - Ok(m) => println!("{m:#?}"), + Ok(m) => { + println!("{m:#?}"); + let temps = m.temperatures(); + if !temps.is_empty() { + print!(" temperatures:"); + for t in &temps { + print!(" {}C", t); + } + println!(); + } + } Err(error) => println!("Cannot retrieve motherboard information: {error:?}"), }, "product" => { diff --git a/src/common/disk.rs b/src/common/disk.rs index 638a0a264..989d90228 100644 --- a/src/common/disk.rs +++ b/src/common/disk.rs @@ -189,6 +189,30 @@ impl Disk { pub fn usage(&self) -> DiskUsage { self.inner.usage() } + + /// Returns the disk's temperature in degrees Celsius. + /// + /// Currently only implemented on Linux (NVMe drives). + /// Returns `None` if the temperature information is not available. + pub fn temperature(&self) -> Option { + self.inner.temperature() + } + + /// Returns the highest temperature (in degrees Celsius) recorded for this disk. + /// + /// Currently only implemented on Linux (NVMe drives). + /// Returns `None` if this information isn't available. + pub fn max(&self) -> Option { + self.inner.max() + } + + /// Returns the critical temperature threshold (in degrees Celsius) for this disk. + /// + /// Currently only implemented on Linux (NVMe drives). + /// Returns `None` if this information isn't available. + pub fn critical(&self) -> Option { + self.inner.critical() + } } /// Disks interface. @@ -411,6 +435,7 @@ impl fmt::Display for DiskKind { /// * `kind` is about refreshing the [`Disk::kind`] information. /// * `storage` is about refreshing the [`Disk::available_space`] and [`Disk::total_space`] information. /// * `io_usage` is about refreshing the [`Disk::usage`] information. +/// * `temperature` is about refreshing the [`Disk::temperature`] information. /// /// ```no_run /// use sysinfo::{Disks, DiskRefreshKind}; @@ -426,6 +451,7 @@ pub struct DiskRefreshKind { kind: bool, storage: bool, io_usage: bool, + temperature: bool, } impl DiskRefreshKind { @@ -439,6 +465,7 @@ impl DiskRefreshKind { /// assert_eq!(r.kind(), false); /// assert_eq!(r.storage(), false); /// assert_eq!(r.io_usage(), false); + /// assert_eq!(r.temperature(), false); /// ``` pub fn nothing() -> Self { Self::default() @@ -454,18 +481,26 @@ impl DiskRefreshKind { /// assert_eq!(r.kind(), true); /// assert_eq!(r.storage(), true); /// assert_eq!(r.io_usage(), true); + /// assert_eq!(r.temperature(), true); /// ``` pub fn everything() -> Self { Self { kind: true, storage: true, io_usage: true, + temperature: true, } } impl_get_set!(DiskRefreshKind, kind, with_kind, without_kind); impl_get_set!(DiskRefreshKind, storage, with_storage, without_storage); impl_get_set!(DiskRefreshKind, io_usage, with_io_usage, without_io_usage); + impl_get_set!( + DiskRefreshKind, + temperature, + with_temperature, + without_temperature + ); } #[cfg(test)] diff --git a/src/common/gpu.rs b/src/common/gpu.rs index 5cd322821..78b3eb4c9 100644 --- a/src/common/gpu.rs +++ b/src/common/gpu.rs @@ -166,7 +166,7 @@ impl core::str::FromStr for PCI { let Some(value) = iter.next() else { return Err(missing_msg); }; - value.parse::().map_err(|_| invalid_msg) + u32::from_str_radix(value, 16).map_err(|_| invalid_msg) } let mut iter = s.split(':'); @@ -282,4 +282,50 @@ impl Gpu { pub fn used_memory(&self) -> Option { self.inner.used_memory() } + + /// Returns the temperature of this GPU in degrees Celsius. + /// + /// On Linux, this information is only available for NVIDIA and AMD GPUs. + /// + /// On macOS and Windows, availability depends on GPU driver support. + /// + /// Returns `None` if the information cannot be retrieved. + pub fn temperature(&self) -> Option { + self.inner.temperature() + } +} + +#[cfg(test)] +mod tests { + use super::PCI; + + #[test] + fn test_pci_from_str() { + // sysfs BDF notation (e.g. `/sys/class/drm/*/device`) + let pci: PCI = "0000:c1:00.0".parse().unwrap(); + assert_eq!( + pci, + PCI { + domain: 0, + bus: 0xc1, + device: 0, + function: 0, + } + ); + assert_eq!(pci.to_string(), "0000:c1:00.0"); + + // NVML's `busId` format (8-digit hex domain) + let pci: PCI = "00000000:65:00.0".parse().unwrap(); + assert_eq!( + pci, + PCI { + domain: 0, + bus: 0x65, + device: 0, + function: 0, + } + ); + + assert!("0000:01:00".parse::().is_err()); + } } diff --git a/src/common/system.rs b/src/common/system.rs index fff359f60..adc92c11d 100644 --- a/src/common/system.rs +++ b/src/common/system.rs @@ -213,6 +213,22 @@ impl System { self.refresh_cpu_specifics(CpuRefreshKind::nothing().with_frequency()) } + /// Refreshes CPUs temperature. + /// + /// Calling this method is the same as calling + /// `system.refresh_cpu_specifics(CpuRefreshKind::nothing().with_temperature())`. + /// + /// ```no_run + /// use sysinfo::System; + /// + /// if let Ok(mut s) = System::new_all() { + /// s.refresh_cpu_temperature(); + /// } + /// ``` + pub fn refresh_cpu_temperature(&mut self) { + self.refresh_cpu_specifics(CpuRefreshKind::nothing().with_temperature()) + } + /// Refreshes the list of CPU. /// /// Normally, this should almost never be needed as it's pretty rare for a computer @@ -1070,6 +1086,14 @@ impl Motherboard { pub fn asset_tag(&self) -> Option { self.inner.asset_tag() } + + /// Returns the motherboard's temperature sensors in degrees Celsius. + /// + /// Currently only implemented on Linux. + /// Returns an empty `Vec` if the information is not available. + pub fn temperatures(&self) -> Vec { + self.inner.temperatures() + } } /// This type allows to retrieve product-related information. @@ -2449,7 +2473,7 @@ pub enum ProcessesToUpdate<'a> { /// information from `/proc//` as well as all the information from `/proc//task//` /// folders. This makes the refresh mechanism a lot slower depending on the number of tasks /// each process has. -/// +/// /// If you don't care about tasks information, use `ProcessRefreshKind::everything().without_tasks()` /// as much as possible. /// @@ -2623,6 +2647,7 @@ It will retrieve the following information: pub struct CpuRefreshKind { usage: bool, frequency: bool, + temperature: bool, } impl CpuRefreshKind { @@ -2635,6 +2660,7 @@ impl CpuRefreshKind { /// /// assert_eq!(r.frequency(), false); /// assert_eq!(r.usage(), false); + /// assert_eq!(r.temperature(), false); /// ``` pub fn nothing() -> Self { Self::default() @@ -2649,16 +2675,24 @@ impl CpuRefreshKind { /// /// assert_eq!(r.frequency(), true); /// assert_eq!(r.usage(), true); + /// assert_eq!(r.temperature(), true); /// ``` pub fn everything() -> Self { Self { usage: true, frequency: true, + temperature: true, } } impl_get_set!(CpuRefreshKind, usage, with_usage, without_usage); impl_get_set!(CpuRefreshKind, frequency, with_frequency, without_frequency); + impl_get_set!( + CpuRefreshKind, + temperature, + with_temperature, + without_temperature + ); } /// Used to determine which memory you want to refresh specifically. @@ -2963,6 +2997,41 @@ impl Cpu { pub fn frequency(&self) -> u64 { self.inner.frequency() } + + /// Returns the CPU's temperature in degrees Celsius. + /// + /// This is currently only implemented on Linux. + /// + /// ```no_run + /// use sysinfo::{System, RefreshKind, CpuRefreshKind}; + /// + /// if let Ok(s) = System::new_with_specifics( + /// RefreshKind::nothing().with_cpu(CpuRefreshKind::everything()), + /// ) { + /// for cpu in s.cpus() { + /// println!("{}", cpu.temperature()); + /// } + /// } + /// ``` + pub fn temperature(&self) -> f32 { + self.inner.temperature() + } + + /// Returns the highest temperature (in degrees Celsius) recorded for this CPU. + /// + /// This is currently only implemented on Linux. + /// Returns `None` if this information isn't available. + pub fn max(&self) -> Option { + self.inner.max() + } + + /// Returns the critical temperature threshold (in degrees Celsius) for this CPU. + /// + /// This is currently only implemented on Linux. + /// Returns `None` if this information isn't available. + pub fn critical(&self) -> Option { + self.inner.critical() + } } #[cfg(test)] diff --git a/src/unix/apple/cpu.rs b/src/unix/apple/cpu.rs index ccbe09d4f..63ea0c794 100644 --- a/src/unix/apple/cpu.rs +++ b/src/unix/apple/cpu.rs @@ -191,6 +191,18 @@ impl CpuInner { self.usage.frequency } + pub(crate) fn temperature(&self) -> f32 { + 0.0 + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } + pub(crate) fn vendor_id(&self) -> &str { &self.vendor_id } diff --git a/src/unix/apple/disk.rs b/src/unix/apple/disk.rs index 78a56492c..7cb346fe7 100644 --- a/src/unix/apple/disk.rs +++ b/src/unix/apple/disk.rs @@ -35,6 +35,9 @@ pub(crate) struct DiskInner { pub(crate) written_bytes: u64, pub(crate) read_bytes: u64, updated: bool, + temperature: Option, + temperature_max: Option, + temperature_critical: Option, uuid: OsString, } @@ -58,6 +61,9 @@ impl Default for DiskInner { written_bytes: 0, read_bytes: 0, updated: false, + temperature: None, + temperature_max: None, + temperature_critical: None, uuid: OsString::new(), } } @@ -149,6 +155,18 @@ impl DiskInner { } } + pub(crate) fn temperature(&self) -> Option { + self.temperature + } + + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } + fn refresh_kind(&mut self, refresh_kind: DiskRefreshKind) { if refresh_kind.kind() && self.type_ == DiskKind::Unknown(-1) { #[cfg(target_os = "macos")] @@ -529,6 +547,9 @@ unsafe fn new_disk( old_written_bytes: 0, updated: true, uuid, + temperature: None, + temperature_max: None, + temperature_critical: None, }; disk.refresh_kind(refresh_kind); diff --git a/src/unix/apple/gpu.rs b/src/unix/apple/gpu.rs index 92e034782..1f46f2801 100644 --- a/src/unix/apple/gpu.rs +++ b/src/unix/apple/gpu.rs @@ -49,6 +49,9 @@ impl GpusInner { let class_code_key = CFString::from_str("class-code"); let pcidebug_key = CFString::from_str("pcidebug"); let perf_key = CFString::from_str("PerformanceStatistics"); + let usage_percent_key = CFString::from_str("Device Utilization %"); + let usage_key = CFString::from_str("Device Utilization"); + let temperature_key = CFString::from_str("Temperature(C)"); while let Some(accelerator) = IOReleaser::new(IOIteratorNext(iterator.inner())) { let mut device = 0; @@ -99,6 +102,7 @@ impl GpusInner { model: None, vendor: None, usage: None, + temperature: None, updated: true, }, }); @@ -149,13 +153,18 @@ impl GpusInner { 0, ) && let Some(usage_dict) = usage.downcast_ref::() && let usage_dict = usage_dict.cast_unchecked::() - && let Some(usage) = usage_dict - .get(&CFString::from_str("Device Utilization %")) - .or_else(|| usage_dict.get(&CFString::from_str("Device Utilization"))) - && let Ok(usage) = usage.downcast::() - && let Some(usage) = usage.as_i64() { - gpu.usage = Some(usage as f32); + if let Some(usage) = usage_dict + .get(&usage_percent_key) + .or_else(|| usage_dict.get(&usage_key)) + .and_then(|usage| usage.as_i64()) + { + gpu.usage = Some(usage as f32); + } + gpu.temperature = usage_dict + .get(&temperature_key) + .and_then(|temperature| temperature.as_f32()) + .filter(|temperature| temperature.is_finite() && *temperature > 0.0); } } } @@ -185,6 +194,7 @@ pub(crate) struct GpuInner { vendor: Option, model: Option, usage: Option, + temperature: Option, pub(crate) updated: bool, } @@ -207,4 +217,7 @@ impl GpuInner { pub(crate) fn used_memory(&self) -> Option { None } + pub(crate) fn temperature(&self) -> Option { + self.temperature + } } diff --git a/src/unix/apple/motherboard.rs b/src/unix/apple/motherboard.rs index 95e05e78d..d75b00eb7 100644 --- a/src/unix/apple/motherboard.rs +++ b/src/unix/apple/motherboard.rs @@ -50,4 +50,8 @@ impl MotherboardInner { pub(crate) fn asset_tag(&self) -> Option { None } + + pub(crate) fn temperatures(&self) -> Vec { + Vec::new() + } } diff --git a/src/unix/bsd/freebsd/cpu.rs b/src/unix/bsd/freebsd/cpu.rs index c31eee33b..a4999361b 100644 --- a/src/unix/bsd/freebsd/cpu.rs +++ b/src/unix/bsd/freebsd/cpu.rs @@ -142,6 +142,18 @@ impl CpuInner { self.frequency } + pub(crate) fn temperature(&self) -> f32 { + 0.0 + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } + pub(crate) fn vendor_id(&self) -> &str { &self.vendor_id } diff --git a/src/unix/bsd/freebsd/disk.rs b/src/unix/bsd/freebsd/disk.rs index b57259891..2274073a8 100644 --- a/src/unix/bsd/freebsd/disk.rs +++ b/src/unix/bsd/freebsd/disk.rs @@ -33,6 +33,9 @@ pub(crate) struct DiskInner { written_bytes: u64, old_written_bytes: u64, updated: bool, + temperature: Option, + temperature_max: Option, + temperature_critical: Option, } #[cfg(test)] @@ -53,6 +56,9 @@ impl Default for DiskInner { written_bytes: 0, old_written_bytes: 0, updated: false, + temperature: None, + temperature_max: None, + temperature_critical: None, } } } @@ -103,6 +109,18 @@ impl DiskInner { total_written_bytes: self.written_bytes, } } + + pub(crate) fn temperature(&self) -> Option { + self.temperature + } + + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } } impl DisksInner { @@ -440,6 +458,9 @@ pub unsafe fn get_all_list( written_bytes: 0, old_written_bytes: 0, updated: true, + temperature: None, + temperature_max: None, + temperature_critical: None, }; // I/O usage is updated for all disks at once at the end. refresh_disk(&mut disk, refresh_kind.without_io_usage()); diff --git a/src/unix/bsd/freebsd/motherboard.rs b/src/unix/bsd/freebsd/motherboard.rs index 098c2ce77..28c51d6b7 100644 --- a/src/unix/bsd/freebsd/motherboard.rs +++ b/src/unix/bsd/freebsd/motherboard.rs @@ -29,4 +29,8 @@ impl MotherboardInner { pub(crate) fn serial_number(&self) -> Option { get_kenv_var(b"smbios.planar.serial\0") } + + pub(crate) fn temperatures(&self) -> Vec { + Vec::new() + } } diff --git a/src/unix/bsd/netbsd/cpu.rs b/src/unix/bsd/netbsd/cpu.rs index 8b110cdd7..bbf0d0304 100644 --- a/src/unix/bsd/netbsd/cpu.rs +++ b/src/unix/bsd/netbsd/cpu.rs @@ -136,6 +136,18 @@ impl CpuInner { self.frequency } + pub(crate) fn temperature(&self) -> f32 { + 0.0 + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } + pub(crate) fn vendor_id(&self) -> &str { &self.vendor_id } diff --git a/src/unix/bsd/netbsd/disk.rs b/src/unix/bsd/netbsd/disk.rs index b2b24d8ae..34ddce828 100644 --- a/src/unix/bsd/netbsd/disk.rs +++ b/src/unix/bsd/netbsd/disk.rs @@ -27,6 +27,9 @@ pub(crate) struct DiskInner { written_bytes: u64, old_written_bytes: u64, updated: bool, + temperature: Option, + temperature_max: Option, + temperature_critical: Option, } #[cfg(test)] @@ -47,6 +50,9 @@ impl Default for DiskInner { written_bytes: 0, old_written_bytes: 0, updated: false, + temperature: None, + temperature_max: None, + temperature_critical: None, } } } @@ -98,6 +104,18 @@ impl DiskInner { } } + pub(crate) fn temperature(&self) -> Option { + self.temperature + } + + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } + fn update_old(&mut self) { self.old_read_bytes = self.read_bytes; self.old_written_bytes = self.written_bytes; @@ -401,6 +419,9 @@ pub unsafe fn get_all_list( written_bytes: 0, old_written_bytes: 0, updated: true, + temperature: None, + temperature_max: None, + temperature_critical: None, }; // I/O usage is updated for all disks at once at the end. refresh_disk(&mut disk, refresh_kind.without_io_usage()); diff --git a/src/unix/bsd/netbsd/motherboard.rs b/src/unix/bsd/netbsd/motherboard.rs index b962f3354..66b90d56a 100644 --- a/src/unix/bsd/netbsd/motherboard.rs +++ b/src/unix/bsd/netbsd/motherboard.rs @@ -35,4 +35,8 @@ impl MotherboardInner { // FIXME: Wrong mib get_sys_value_str_by_name(b"machdep.dmi.board-serial\0") } + + pub(crate) fn temperatures(&self) -> Vec { + Vec::new() + } } diff --git a/src/unix/linux/cpu.rs b/src/unix/linux/cpu.rs index cb71f648a..99099f27e 100644 --- a/src/unix/linux/cpu.rs +++ b/src/unix/linux/cpu.rs @@ -3,8 +3,9 @@ #![allow(clippy::too_many_arguments)] use std::collections::{HashMap, HashSet}; -use std::fs::File; +use std::fs::{File, read_dir}; use std::io::{BufRead, BufReader, Read}; +use std::path::{Path, PathBuf}; use std::time::Instant; use crate::sys::utils::to_u64; @@ -22,6 +23,8 @@ pub(crate) struct CpusWrapper { got_cpu_frequency: bool, /// This field is needed to prevent updating when not enough time passed since last update. last_update: Option, + /// Resolved once when `self.cpus` is first populated. + cpu_temperature_sensor: Option, } impl CpusWrapper { @@ -31,6 +34,7 @@ impl CpusWrapper { cpus: Vec::with_capacity(4), got_cpu_frequency: false, last_update: None, + cpu_temperature_sensor: None, } } @@ -180,6 +184,18 @@ impl CpusWrapper { self.got_cpu_frequency = true; } + + if first || refresh_kind.temperature() { + if first { + self.cpu_temperature_sensor = + find_cpu_temperature_sensor(Path::new("/sys/class/hwmon")); + } + if let Some(sensor) = &self.cpu_temperature_sensor { + for cpu in &mut self.cpus { + cpu.inner.update_temperature(sensor); + } + } + } } pub(crate) fn get_global_raw_times(&self) -> (u64, u64) { @@ -366,6 +382,9 @@ pub(crate) struct CpuInner { usage: CpuUsage, pub(crate) name: String, pub(crate) frequency: u64, + pub(crate) temperature: f32, + pub(crate) temperature_max: Option, + pub(crate) temperature_critical: Option, pub(crate) vendor_id: String, pub(crate) brand: String, } @@ -393,6 +412,9 @@ impl CpuInner { ), name: name.to_owned(), frequency, + temperature: 0.0, + temperature_max: None, + temperature_critical: None, vendor_id, brand, } @@ -429,6 +451,21 @@ impl CpuInner { self.frequency } + /// Returns the CPU temperature in degrees Celsius. + pub(crate) fn temperature(&self) -> f32 { + self.temperature + } + + /// Returns the highest temperature (in degrees Celsius) recorded for this CPU. + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + /// Returns the critical temperature threshold (in degrees Celsius) for this CPU, if known. + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } + pub(crate) fn vendor_id(&self) -> &str { &self.vendor_id } @@ -436,6 +473,16 @@ impl CpuInner { pub(crate) fn brand(&self) -> &str { &self.brand } + + fn update_temperature(&mut self, sensor: &CpuTemperatureSensor) { + let Some(current) = read_temperature_celsius(&sensor.input) else { + return; + }; + self.temperature = current; + self.temperature_max = read_temperature_celsius(&sensor.highest) + .or_else(|| Some(self.temperature_max.unwrap_or(current).max(current))); + self.temperature_critical = sensor.critical; + } } pub(crate) fn get_cpu_frequency(cpu_core_index: usize) -> u64 { @@ -474,6 +521,93 @@ pub(crate) fn get_cpu_frequency(cpu_core_index: usize) -> u64 { .unwrap_or_default() } +/// Names of the `hwmon` drivers which expose **CPU** temperature sensors, in order of preference +const CPU_HWMON_DRIVERS: &[&str] = &["coretemp", "k10temp", "zenpower", "cpu_thermal", "acpitz"]; +/// Labels of a sensor reporting a whole-package temperature, in order of preference. +const CPU_PACKAGE_TEMP_LABELS: &[&str] = &["Package id 0", "Tctl", "Tdie"]; + +fn read_line(path: &Path) -> Option { + let mut buf = String::with_capacity(32); + File::open(path).ok()?.read_to_string(&mut buf).ok()?; + let len = buf.trim_end().len(); + buf.truncate(len); + Some(buf) +} + +/// Reads a `tempN_input`/`tempN_highest` sysfs file and converts it to Celsius. +fn read_temperature_celsius(path: &Path) -> Option { + let mut buf = [0u8; 32]; + let n = File::open(path).ok()?.read(&mut buf).ok()?; + std::str::from_utf8(&buf[..n]) + .ok()? + .trim() + .parse::() + .ok() + .map(|milli_celsius| milli_celsius as f32 / 1000.0) +} + +/// The sysfs files backing the CPU's temperature. +#[derive(Clone, Debug, PartialEq)] +struct CpuTemperatureSensor { + input: PathBuf, + /// May not exist, in which case `update_temperature` falls back to a running maximum. + highest: PathBuf, + critical: Option, +} + +fn find_cpu_temperature_sensor(hwmon_root: &Path) -> Option { + let hwmon_dir = read_dir(hwmon_root) + .ok()? + .flatten() + .filter_map(|entry| { + let path = entry.path(); + let name = read_line(&path.join("name"))?; + let priority = CPU_HWMON_DRIVERS + .iter() + .position(|&driver| driver == name)?; + Some((priority, path)) + }) + .min_by_key(|(priority, _)| *priority) + .map(|(_, path)| path)?; + + let mut sensors: Vec<(u32, Option, CpuTemperatureSensor)> = read_dir(&hwmon_dir) + .ok()? + .flatten() + .filter_map(|entry| { + let input = entry.path(); + let id = input + .file_name()? + .to_str()? + .strip_prefix("temp")? + .strip_suffix("_input")? + .parse::() + .ok()?; + let label = read_line(&hwmon_dir.join(format!("temp{id}_label"))); + let critical = read_temperature_celsius(&hwmon_dir.join(format!("temp{id}_crit"))); + Some(( + id, + label, + CpuTemperatureSensor { + input, + highest: hwmon_dir.join(format!("temp{id}_highest")), + critical, + }, + )) + }) + .collect(); + sensors.sort_by_key(|(id, ..)| *id); + + CPU_PACKAGE_TEMP_LABELS + .iter() + .find_map(|&wanted| { + sensors + .iter() + .find(|(_, label, _)| label.as_deref() == Some(wanted)) + }) + .or_else(|| sensors.first()) + .map(|(_, _, sensor)| sensor.clone()) +} + #[allow(unused_assignments)] pub(crate) fn get_physical_core_count() -> Result { let mut s = String::new(); @@ -971,7 +1105,7 @@ BogoMIPS : 38.40 processor : 7 BogoMIPS : 38.40 -Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt +Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt CPU implementer : 0x41 CPU architecture: 7 CPU variant : 0x0 & 0x3 diff --git a/src/unix/linux/disk.rs b/src/unix/linux/disk.rs index 98911efb8..7b34218d9 100644 --- a/src/unix/linux/disk.rs +++ b/src/unix/linux/disk.rs @@ -49,6 +49,9 @@ pub(crate) struct DiskInner { written_bytes: u64, read_bytes: u64, updated: bool, + temperature: Option, + temperature_max: Option, + temperature_critical: Option, } #[cfg(test)] @@ -69,6 +72,9 @@ impl Default for DiskInner { written_bytes: 0, read_bytes: 0, updated: false, + temperature: None, + temperature_max: None, + temperature_critical: None, } } } @@ -149,6 +155,13 @@ impl DiskInner { } } + if refresh_kind.temperature() { + let (temp, max, critical) = get_disk_temperature(&self.device_name); + self.temperature = temp; + self.temperature_max = max; + self.temperature_critical = critical; + } + true } @@ -160,6 +173,18 @@ impl DiskInner { total_written_bytes: self.written_bytes, } } + + pub(crate) fn temperature(&self) -> Option { + self.temperature + } + + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } } impl DisksInner { @@ -279,6 +304,9 @@ fn new_disk( read_bytes: 0, written_bytes: 0, updated: true, + temperature: None, + temperature_max: None, + temperature_critical: None, }, }; disk.inner @@ -533,6 +561,69 @@ fn disk_stats_inner(content: &str) -> HashMap { data } +/// Reads a `tempN_input` sysfs file and converts it to Celsius. +fn read_temperature_celsius(path: &Path) -> Option { + let s = std::fs::read_to_string(path).ok()?; + let milli = s.trim().parse::().ok()?; + Some(milli as f32 / 1000.0) +} + +/// Strips the partition suffix from a device name (e.g. `nvme0n1p1` → `nvme0n1`, `sda1` → `sda`). +fn strip_partition_suffix(name: &str) -> &str { + // NVMe: nvme0n1p1 → nvme0n1 + if let Some(pos) = name.rfind('p') + && pos > 0 + && name[pos + 1..].chars().all(|c| c.is_ascii_digit()) + { + return &name[..pos]; + } + // Other: sda1 → sda, vda1 → vda + let trimmed = name.trim_end_matches(|c: char| c.is_ascii_digit()); + if trimmed.is_empty() { name } else { trimmed } +} + +fn get_disk_temperature(device_name: &OsStr) -> (Option, Option, Option) { + let name = match device_name.to_str() { + Some(n) => n.strip_prefix("/dev/").unwrap_or(n), + None => return (None, None, None), + }; + + let read_all = |base: &str| -> (Option, Option, Option) { + let device_dir = Path::new("/sys/block").join(base).join("device"); + // NVMe drives expose temperature under device/hwmonN/. + let dir = std::fs::read_dir(&device_dir) + .ok() + .and_then(|entries| { + entries.flatten().map(|e| e.path()).find(|p| { + p.is_dir() + && p.file_name() + .and_then(|n| n.to_str()) + .map(|n| n.starts_with("hwmon")) + .unwrap_or(false) + }) + }) + .unwrap_or(device_dir); + let temp = read_temperature_celsius(&dir.join("temp1_input")); + let max = read_temperature_celsius(&dir.join("temp1_max")); + let critical = read_temperature_celsius(&dir.join("temp1_crit")); + (temp, max, critical) + }; + + // Try the full name first (for non-partitioned devices). + let (temp, max, critical) = read_all(name); + if temp.is_some() { + return (temp, max, critical); + } + + // Try the base device name (strip partition suffix). + let base = strip_partition_suffix(name); + if base != name { + return read_all(base); + } + + (None, None, None) +} + #[cfg(test)] mod test { use super::{DiskStat, disk_stats_inner}; diff --git a/src/unix/linux/gpu.rs b/src/unix/linux/gpu.rs index 9190c2206..d622c2fc5 100644 --- a/src/unix/linux/gpu.rs +++ b/src/unix/linux/gpu.rs @@ -36,6 +36,7 @@ pub(crate) struct GpuInner { total_memory: Option, used_memory: Option, usage: Option, + temperature: Option, model: Option, vendor: String, pci: PCI, @@ -48,6 +49,7 @@ impl GpuInner { total_memory: None, used_memory: None, usage: None, + temperature: None, model: model.cloned(), vendor: vendor.to_owned(), pci, @@ -73,6 +75,9 @@ impl GpuInner { pub(crate) fn used_memory(&self) -> Option { self.used_memory } + pub(crate) fn temperature(&self) -> Option { + self.temperature + } } impl GpusInner { @@ -298,6 +303,15 @@ fn get_amd_info(gpu: &mut GpuInner, buffer: &mut String, path: &Path) { if read_file(path.join("mem_info_vram_total"), buffer).is_ok() { gpu.total_memory = buffer.trim().parse::().ok(); } + gpu.temperature = read_dir(path.join("hwmon")) + .ok() + .and_then(|dir| { + dir.flatten().find_map(|entry| { + read_file(entry.path().join("temp1_input"), buffer).ok()?; + buffer.trim().parse::().ok() + }) + }) + .map(|milli_c| milli_c / 1_000.0); gpu.updated = true; } @@ -331,6 +345,7 @@ fn convert_to_str(data: &[libc::c_char]) -> Option> { mod nvidia { use super::*; use libc::{c_char, c_int, c_uint, c_ulonglong, c_void}; + use std::ffi::CStr; use std::mem::MaybeUninit; use std::ptr::null_mut; @@ -372,11 +387,14 @@ mod nvidia { unsafe extern "C" fn(NvmlDeviceHandle, *mut NvmlUtilization) -> NvmlReturn; type NvmlDeviceGetMemoryInfoFn = unsafe extern "C" fn(NvmlDeviceHandle, *mut NvmlMemory) -> NvmlReturn; + type NvmlDeviceGetTemperatureFn = + unsafe extern "C" fn(NvmlDeviceHandle, c_uint, *mut c_uint) -> NvmlReturn; type NvmlDeviceGetPciInfo = unsafe extern "C" fn(NvmlDeviceHandle, *mut NvmlPciInfo) -> NvmlReturn; const NVML_SUCCESS: NvmlReturn = 0; const NVML_DEVICE_NAME_V2_BUFFER_SIZE: usize = 96; + const NVML_TEMPERATURE_GPU: c_uint = 0; pub(crate) struct NvmlLib { lib_handle: *mut c_void, @@ -387,25 +405,31 @@ mod nvidia { get_name: NvmlDeviceGetNameFn, get_utilization: NvmlDeviceGetUtilizationRatesFn, get_memory_info: NvmlDeviceGetMemoryInfoFn, + get_temperature: NvmlDeviceGetTemperatureFn, get_pci_info: NvmlDeviceGetPciInfo, } impl NvmlLib { pub(crate) unsafe fn load() -> Option { - let lib_name = c"libnvidia-ml.so.1"; + const LIB_PATHS: &[&CStr] = &[ + c"libnvidia-ml.so.1", + c"/run/opengl-driver/lib/libnvidia-ml.so.1", + ]; unsafe { - let lib_handle = libc::dlopen(lib_name.as_ptr(), libc::RTLD_NOW); - if lib_handle.is_null() { - sysinfo_debug!("Failed to find or load {lib_name:?}"); - return None; - } - match Self::load_symbols(lib_handle) { - Some(ret) => Some(ret), - None => { - libc::dlclose(lib_handle); - None + for lib_name in LIB_PATHS { + let lib_handle = libc::dlopen(lib_name.as_ptr(), libc::RTLD_NOW); + if lib_handle.is_null() { + sysinfo_debug!("Failed to find or load {lib_name:?}"); + continue; + } + match Self::load_symbols(lib_handle) { + Some(ret) => return Some(ret), + None => { + libc::dlclose(lib_handle); + } } } + None } } @@ -436,6 +460,11 @@ mod nvidia { c"nvmlDeviceGetMemoryInfo", NvmlDeviceGetMemoryInfoFn, )?, + get_temperature: load_sym!( + lib_handle, + c"nvmlDeviceGetTemperature", + NvmlDeviceGetTemperatureFn, + )?, get_pci_info: load_sym!( lib_handle, c"nvmlDeviceGetPciInfo_v3", @@ -546,6 +575,17 @@ mod nvidia { gpu.total_memory = Some(mem_info.total); gpu.used_memory = Some(mem_info.used); } + + let mut temperature: c_uint = 0; + if (self.inner.get_temperature)( + device_handle, + NVML_TEMPERATURE_GPU, + &mut temperature, + ) == NVML_SUCCESS + { + gpu.temperature = Some(temperature as f32); + } + gpu.updated = true; } } diff --git a/src/unix/linux/motherboard.rs b/src/unix/linux/motherboard.rs index f229d9421..88dff0c8b 100644 --- a/src/unix/linux/motherboard.rs +++ b/src/unix/linux/motherboard.rs @@ -1,7 +1,8 @@ // Take a look at the license at the top of the repository in the LICENSE file. use crate::Error; -use std::fs::{read, read_to_string}; +use std::fs::{read, read_dir, read_to_string}; +use std::path::Path; pub(crate) struct MotherboardInner; @@ -45,6 +46,44 @@ impl MotherboardInner { .ok() .map(|s| s.trim().to_owned()) } + + pub(crate) fn temperatures(&self) -> Vec { + read_motherboard_temperatures() + } +} + +fn read_motherboard_temperatures() -> Vec { + let hwmon_root = Path::new("/sys/class/hwmon"); + let Ok(entries) = read_dir(hwmon_root) else { + return Vec::new(); + }; + + let mut temps = Vec::new(); + + for entry in entries.flatten() { + let path = entry.path(); + let name = read_to_string(path.join("name")).ok(); + if name.as_deref().map(|n| n.trim()) != Some("acpitz") { + continue; + } + + let Ok(sensors) = read_dir(&path) else { + continue; + }; + for sensor in sensors.flatten() { + let fname = sensor.file_name(); + let fname_str = fname.to_string_lossy(); + if fname_str.starts_with("temp") + && fname_str.ends_with("_input") + && let Ok(raw) = read_to_string(sensor.path()) + && let Ok(milli) = raw.trim().parse::() + { + temps.push(milli as f32 / 1000.0); + } + } + } + + temps } // Parses the first entry of the file `/proc/device-tree/compatible`, to extract the vendor and diff --git a/src/unknown/cpu.rs b/src/unknown/cpu.rs index 9ef70f6f9..ca68e5685 100644 --- a/src/unknown/cpu.rs +++ b/src/unknown/cpu.rs @@ -15,6 +15,18 @@ impl CpuInner { 0 } + pub(crate) fn temperature(&self) -> f32 { + 0.0 + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } + pub(crate) fn vendor_id(&self) -> &str { "" } diff --git a/src/unknown/disk.rs b/src/unknown/disk.rs index fa9197fc3..855130198 100644 --- a/src/unknown/disk.rs +++ b/src/unknown/disk.rs @@ -59,6 +59,18 @@ impl DiskInner { pub(crate) fn usage(&self) -> DiskUsage { DiskUsage::default() } + + pub(crate) fn temperature(&self) -> Option { + None + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } } pub(crate) struct DisksInner; diff --git a/src/unknown/gpu.rs b/src/unknown/gpu.rs index a989ff98a..d7b39a77a 100644 --- a/src/unknown/gpu.rs +++ b/src/unknown/gpu.rs @@ -39,4 +39,7 @@ impl GpuInner { pub(crate) fn used_memory(&self) -> Option { unreachable!() } + pub(crate) fn temperature(&self) -> Option { + unreachable!() + } } diff --git a/src/unknown/motherboard.rs b/src/unknown/motherboard.rs index 50ff4655d..2e1d2a8a1 100644 --- a/src/unknown/motherboard.rs +++ b/src/unknown/motherboard.rs @@ -28,4 +28,8 @@ impl MotherboardInner { pub(crate) fn asset_tag(&self) -> Option { unreachable!() } + + pub(crate) fn temperatures(&self) -> Vec { + Vec::new() + } } diff --git a/src/windows/cpu.rs b/src/windows/cpu.rs index f5ad97d44..a9f0f1e14 100644 --- a/src/windows/cpu.rs +++ b/src/windows/cpu.rs @@ -347,6 +347,18 @@ impl CpuInner { self.frequency } + pub(crate) fn temperature(&self) -> f32 { + 0.0 + } + + pub(crate) fn max(&self) -> Option { + None + } + + pub(crate) fn critical(&self) -> Option { + None + } + pub(crate) fn vendor_id(&self) -> &str { &self.vendor_id } diff --git a/src/windows/disk.rs b/src/windows/disk.rs index 12c02e492..ab2d6bd85 100644 --- a/src/windows/disk.rs +++ b/src/windows/disk.rs @@ -136,6 +136,9 @@ pub(crate) struct DiskInner { written_bytes: u64, read_bytes: u64, updated: bool, + temperature: Option, + temperature_max: Option, + temperature_critical: Option, } #[cfg(test)] @@ -157,6 +160,9 @@ impl Default for DiskInner { written_bytes: 0, read_bytes: 0, updated: false, + temperature: None, + temperature_max: None, + temperature_critical: None, } } } @@ -236,6 +242,18 @@ impl DiskInner { total_written_bytes: self.written_bytes, } } + + pub(crate) fn temperature(&self) -> Option { + self.temperature + } + + pub(crate) fn max(&self) -> Option { + self.temperature_max + } + + pub(crate) fn critical(&self) -> Option { + self.temperature_critical + } } pub(crate) struct DisksInner { @@ -373,6 +391,9 @@ pub(crate) unsafe fn get_list( read_bytes: 0, written_bytes: 0, updated: true, + temperature: None, + temperature_max: None, + temperature_critical: None, }; disk.refresh_specifics(refreshes); disks.push(Disk { inner: disk }); diff --git a/src/windows/gpu.rs b/src/windows/gpu.rs index 7c30a5acb..6b196fc62 100644 --- a/src/windows/gpu.rs +++ b/src/windows/gpu.rs @@ -4,9 +4,10 @@ use std::collections::HashMap; use std::mem::{MaybeUninit, zeroed}; use windows::Wdk::Graphics::Direct3D::{ - D3DKMT_ADAPTERADDRESS, D3DKMT_CLOSEADAPTER, D3DKMT_OPENADAPTERFROMLUID, - D3DKMT_QUERYADAPTERINFO, D3DKMTCloseAdapter, D3DKMTOpenAdapterFromLuid, D3DKMTQueryAdapterInfo, - KMTQAITYPE_ADAPTERADDRESS, + D3DKMT_ADAPTER_PERFDATA, D3DKMT_ADAPTERADDRESS, D3DKMT_CLOSEADAPTER, + D3DKMT_OPENADAPTERFROMLUID, D3DKMT_QUERYADAPTERINFO, D3DKMTCloseAdapter, + D3DKMTOpenAdapterFromLuid, D3DKMTQueryAdapterInfo, KMTQAITYPE_ADAPTERADDRESS, + KMTQAITYPE_ADAPTERPERFDATA, }; use windows::Win32::Devices::DeviceAndDriverInstallation::{ DIGCF_PRESENT, GUID_DEVCLASS_DISPLAY, HDEVINFO, SETUP_DI_REGISTRY_PROPERTY, SP_DEVINFO_DATA, @@ -31,6 +32,7 @@ pub(crate) struct GpuInner { total_memory: Option, used_memory: Option, usage: Option, + temperature: Option, model: Option, vendor: Option, pci: PCI, @@ -57,6 +59,9 @@ impl GpuInner { pub(crate) fn used_memory(&self) -> Option { self.used_memory } + pub(crate) fn temperature(&self) -> Option { + self.temperature + } } pub(crate) struct GpusInner { @@ -116,6 +121,7 @@ impl GpusInner { let Ok(desc) = adapter.GetDesc1() else { continue; }; + let luid_adapter = LUIDAdapter::new(desc.AdapterLuid); let gpu = match self .gpus .iter_mut() @@ -130,8 +136,9 @@ impl GpusInner { pcis = get_all_pcis(); } if let Some(ref pcis) = pcis - && let Some(addr) = LUIDAdapter::new(desc.AdapterLuid) - .and_then(|adapter| adapter.query()) + && let Some(addr) = luid_adapter + .as_ref() + .and_then(|adapter| adapter.address()) // Why not using the `addr` info instead of iterating through PCIs? // Because it might return a "fake" GPU, and PCIs allow us to filter. && let Some(pci) = pcis.iter().find(|pci| { @@ -148,6 +155,7 @@ impl GpusInner { total_memory: None, used_memory: None, usage: None, + temperature: None, updated: true, luid: desc.AdapterLuid, }; @@ -160,6 +168,9 @@ impl GpusInner { }; gpu.total_memory = Some(desc.DedicatedVideoMemory as u64); + gpu.temperature = luid_adapter + .as_ref() + .and_then(|adapter| adapter.temperature()); if let Ok(adapter3) = adapter.cast::() { let mut mem = MaybeUninit::::uninit(); @@ -382,7 +393,7 @@ impl LUIDAdapter { } } - unsafe fn query(self) -> Option { + unsafe fn address(&self) -> Option { let mut address = D3DKMT_ADAPTERADDRESS::default(); let mut query = D3DKMT_QUERYADAPTERINFO { @@ -400,6 +411,25 @@ impl LUIDAdapter { } } } + + unsafe fn temperature(&self) -> Option { + let mut perf_data = D3DKMT_ADAPTER_PERFDATA::default(); + + let mut query = D3DKMT_QUERYADAPTERINFO { + hAdapter: self.0.hAdapter, + Type: KMTQAITYPE_ADAPTERPERFDATA, + pPrivateDriverData: &mut perf_data as *mut _ as _, + PrivateDriverDataSize: size_of::() as u32, + }; + + unsafe { + if D3DKMTQueryAdapterInfo(&mut query).is_ok() && perf_data.Temperature != 0 { + Some(perf_data.Temperature as f32 / 10.0) + } else { + None + } + } + } } impl Drop for LUIDAdapter { diff --git a/src/windows/motherboard.rs b/src/windows/motherboard.rs index 523e683d5..78416993c 100644 --- a/src/windows/motherboard.rs +++ b/src/windows/motherboard.rs @@ -72,4 +72,8 @@ impl MotherboardInner { .copied() .map(str::to_string) } + + pub(crate) fn temperatures(&self) -> Vec { + Vec::new() + } }