diff options
Diffstat (limited to 'vendor/sysinfo/src/linux/component.rs')
-rw-r--r-- | vendor/sysinfo/src/linux/component.rs | 405 |
1 files changed, 284 insertions, 121 deletions
diff --git a/vendor/sysinfo/src/linux/component.rs b/vendor/sysinfo/src/linux/component.rs index 3a10588e5..7815103b4 100644 --- a/vendor/sysinfo/src/linux/component.rs +++ b/vendor/sysinfo/src/linux/component.rs @@ -1,143 +1,309 @@ // Take a look at the license at the top of the repository in the LICENSE file. +// Information about values readable from `hwmon` sysfs. +// +// Values in /sys/class/hwmonN are `c_long` or `c_ulong` +// transposed to rust we only read `u32` or `i32` values. use crate::ComponentExt; use std::collections::HashMap; -use std::fs::{metadata, read_dir, File}; +use std::fs::{read_dir, File}; use std::io::Read; use std::path::{Path, PathBuf}; #[doc = include_str!("../../md_doc/component.md")] +#[derive(Default)] pub struct Component { - temperature: f32, - max: f32, - critical: Option<f32>, + /// Optional associated device of a `Component`. + device_model: Option<String>, + /// The chip name. + /// + /// Kernel documentation extract: + /// ```txt + /// This should be a short, lowercase string, not containing + /// whitespace, dashes, or the wildcard character '*'. + /// This attribute represents the chip name. It is the only + /// mandatory attribute. + /// I2C devices get this attribute created automatically. + /// ``` + name: String, + /// Temperature current value + /// - Read in: `temp[1-*]_input`. + /// - Unit: read as millidegree Celsius converted to Celsius. + temperature: Option<f32>, + /// Maximum value computed by sysinfo + max: Option<f32>, + /// Max threshold provided by the chip/kernel + /// - Read in:`temp[1-*]_max` + /// - Unit: read as millidegree Celsius converted to Celsius. + threshold_max: Option<f32>, + /// Min threshold provided by the chip/kernel. + /// - Read in:`temp[1-*]_min` + /// - Unit: read as millidegree Celsius converted to Celsius. + threshold_min: Option<f32>, + /// Critical threshold provided by the chip/kernel previous user write. + /// Read in `temp[1-*]_crit`: + /// Typically greater than corresponding temp_max values. + /// - Unit: read as millidegree Celsius converted to Celsius. + threshold_critical: Option<f32>, + /// Sensor type, not common but can exist! + /// + /// Read in: `temp[1-*]_type` Sensor type selection. + /// Values integer: + /// - 1: CPU embedded diode + /// - 2: 3904 transistor + /// - 3: thermal diode + /// - 4: thermistor + /// - 5: AMD AMDSI + /// - 6: Intel PECI + /// Not all types are supported by all chips + sensor_type: Option<TermalSensorType>, + /// Component Label + /// + /// For formating detail see `Component::label` function docstring. + /// + /// ## Linux implementation details + /// + /// read n: `temp[1-*]_label` Suggested temperature channel label. + /// Value: Text string + /// + /// Should only be created if the driver has hints about what + /// this temperature channel is being used for, and user-space + /// doesn't. In all other cases, the label is provided by user-space. label: String, - input_file: PathBuf, + // TODO: not used now. + // Historical minimum temperature + // - Read in:`temp[1-*]_lowest + // - Unit: millidegree Celsius + // + // Temperature critical min value, typically lower than + // corresponding temp_min values. + // - Read in:`temp[1-*]_lcrit` + // - Unit: millidegree Celsius + // + // Temperature emergency max value, for chips supporting more than + // two upper temperature limits. Must be equal or greater than + // corresponding temp_crit values. + // - temp[1-*]_emergency + // - Unit: millidegree Celsius + /// File to read current temperature shall be `temp[1-*]_input` + /// It may be absent but we don't continue if absent. + input_file: Option<PathBuf>, + /// `temp[1-*]_highest file` to read if disponnible highest value. + highest_file: Option<PathBuf>, } +// Read arbitrary data from sysfs. fn get_file_line(file: &Path, capacity: usize) -> Option<String> { let mut reader = String::with_capacity(capacity); - if let Ok(mut f) = File::open(file) { - if f.read_to_string(&mut reader).is_ok() { - Some(reader) - } else { - None - } - } else { - None + let mut f = File::open(file).ok()?; + f.read_to_string(&mut reader).ok()?; + reader.truncate(reader.trim_end().len()); + Some(reader) +} + +/// Designed at first for reading an `i32` or `u32` aka `c_long` +/// from a `/sys/class/hwmon` sysfs file. +fn read_number_from_file<N>(file: &Path) -> Option<N> +where + N: std::str::FromStr, +{ + let mut reader = [0u8; 32]; + let mut f = File::open(file).ok()?; + let n = f.read(&mut reader).ok()?; + // parse and trim would complain about `\0`. + let number = &reader[..n]; + let number = std::str::from_utf8(number).ok()?; + let number = number.trim(); + // Assert that we cleaned a little bit that string. + if cfg!(feature = "debug") { + assert!(!number.contains('\n') && !number.contains('\0')); } + number.parse().ok() } -fn is_file<T: AsRef<Path>>(path: T) -> bool { - metadata(path).ok().map(|m| m.is_file()).unwrap_or(false) +// Read a temperature from a `tempN_item` sensor form the sysfs. +// number returned will be in mili-celsius. +// +// Don't call it on `label`, `name` or `type` file. +#[inline] +fn get_temperature_from_file(file: &Path) -> Option<f32> { + let temp = read_number_from_file(file); + convert_temp_celsius(temp) } -fn append_files(components: &mut Vec<Component>, folder: &Path) { - let mut matchings: HashMap<u32, Vec<String>> = HashMap::with_capacity(10); +/// Takes a raw temperature in mili-celsius and convert it to celsius +#[inline] +fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> { + temp.map(|n| (n as f32) / 1000f32) +} - if let Ok(dir) = read_dir(folder) { - for entry in dir.flatten() { - let entry = entry.path(); - if entry.is_dir() - || !entry - .file_name() - .and_then(|x| x.to_str()) - .unwrap_or("") - .starts_with("temp") - { - continue; - } - if let Some(entry) = entry.file_name() { - if let Some(entry) = entry.to_str() { - let mut parts = entry.split('_'); - if let Some(Some(id)) = parts.next().map(|s| s[4..].parse::<u32>().ok()) { - matchings - .entry(id) - .or_insert_with(|| Vec::with_capacity(5)) - .push( - parts - .next() - .map(|s| format!("_{}", s)) - .unwrap_or_else(String::new), - ); - } - } - } +/// Information about thermal sensor. It may be unavailable as it's +/// kernel module and chip dependant. +enum TermalSensorType { + /// 1: CPU embedded diode + CPUEmbeddedDiode, + /// 2: 3904 transistor + Transistor3904, + /// 3: thermal diode + ThermalDiode, + /// 4: thermistor + Thermistor, + /// 5: AMD AMDSI + AMDAMDSI, + /// 6: Intel PECI + IntelPECI, + /// Not all types are supported by all chips so we keep space for + /// unknown sensors. + Unknown(u8), +} + +impl From<u8> for TermalSensorType { + fn from(input: u8) -> Self { + match input { + 0 => Self::CPUEmbeddedDiode, + 1 => Self::Transistor3904, + 3 => Self::ThermalDiode, + 4 => Self::Thermistor, + 5 => Self::AMDAMDSI, + 6 => Self::IntelPECI, + n => Self::Unknown(n), } - for (key, val) in &matchings { - let mut found_input = None; - let mut found_label = None; - for (pos, v) in val.iter().enumerate() { - match v.as_str() { - // raspberry has empty string for temperature input - "_input" | "" => { - found_input = Some(pos); - } - "_label" => { - found_label = Some(pos); - } - _ => {} - } - } - if let (Some(_), Some(found_input)) = (found_label, found_input) { - let mut p_label = folder.to_path_buf(); - let mut p_input = folder.to_path_buf(); - let mut p_crit = folder.to_path_buf(); - let mut p_max = folder.to_path_buf(); - - p_label.push(&format!("temp{}_label", key)); - p_input.push(&format!("temp{}{}", key, val[found_input])); - p_max.push(&format!("temp{}_max", key)); - p_crit.push(&format!("temp{}_crit", key)); - if is_file(&p_input) { - let label = get_file_line(p_label.as_path(), 10) - .unwrap_or_else(|| format!("Component {}", key)) // needed for raspberry pi - .replace('\n', ""); - let max = get_file_line(p_max.as_path(), 10).map(|max| { - max.replace('\n', "").parse::<f32>().unwrap_or(100_000f32) / 1000f32 - }); - let crit = get_file_line(p_crit.as_path(), 10).map(|crit| { - crit.replace('\n', "").parse::<f32>().unwrap_or(100_000f32) / 1000f32 - }); - components.push(Component::new(label, p_input.as_path(), max, crit)); - } + } +} + +/// Check given `item` dispatch to read the right `file` with the right parsing and store data in +/// given `component`. `id` is provided for `label` creation. +fn fill_component(component: &mut Component, item: &str, folder: &Path, file: &str) { + let hwmon_file = folder.join(file); + match item { + "type" => { + component.sensor_type = + read_number_from_file::<u8>(&hwmon_file).map(TermalSensorType::from) + } + "input" => { + let temperature = get_temperature_from_file(&hwmon_file); + component.input_file = Some(hwmon_file); + component.temperature = temperature; + // Maximum know try to get it from `highest` if not available + // use current temperature + if component.max.is_none() { + component.max = temperature; } } + "label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(), + "highest" => { + component.max = get_temperature_from_file(&hwmon_file).or(component.temperature); + component.highest_file = Some(hwmon_file); + } + "max" => component.threshold_max = get_temperature_from_file(&hwmon_file), + "min" => component.threshold_min = get_temperature_from_file(&hwmon_file), + "crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file), + _ => { + sysinfo_debug!( + "This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}", + hwmon_file, + ); + } } } impl Component { - /// Creates a new component with the given information. - pub(crate) fn new( - label: String, - input_path: &Path, - max: Option<f32>, - critical: Option<f32>, - ) -> Component { - let mut c = Component { - temperature: 0f32, + /// Read out `hwmon` info (hardware monitor) from `folder` + /// to get values' path to be used on refresh as well as files containing `max`, + /// `critical value` and `label`. Then we store everything into `components`. + /// + /// Note that a thermal [Component] must have a way to read its temperature. + /// If not, it will be ignored and not added into `components`. + /// + /// ## What is read: + /// + /// - Mandatory: `name` the name of the `hwmon`. + /// - Mandatory: `tempN_input` Drop [Component] if missing + /// - Optional: sensor `label`, in the general case content of `tempN_label` + /// see below for special cases + /// - Optional: `label` + /// - Optional: `/device/model` + /// - Optional: hightest historic value in `tempN_hightest`. + /// - Optional: max threshold value defined in `tempN_max` + /// - Optional: critical threshold value defined in `tempN_crit` + /// + /// Where `N` is a u32 associated to a sensor like `temp1_max`, `temp1_input`. + /// + /// ## Doc to Linux kernel API. + /// + /// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html + /// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces + /// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html + fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> { + let dir = read_dir(folder).ok()?; + let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10); + for entry in dir.flatten() { + let entry = entry.path(); + let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or(""); + if entry.is_dir() || !filename.starts_with("temp") { + continue; + } + + let (id, item) = filename.split_once('_')?; + let id = id.get(4..)?.parse::<u32>().ok()?; + + let component = matchings.entry(id).or_insert_with(Component::default); + let name = get_file_line(&folder.join("name"), 16); + component.name = name.unwrap_or_default(); + let device_model = get_file_line(&folder.join("device/model"), 16); + component.device_model = device_model; + fill_component(component, item, folder, filename); + } + let compo = matchings + .into_iter() + .map(|(id, mut c)| { + // sysinfo expose a generic interface with a `label`. + // Problem: a lot of sensors don't have a label or a device model! ¯\_(ツ)_/¯ + // So let's pretend we have a unique label! + // See the table in `Component::label` documentation for the table detail. + c.label = c.format_label("temp", id); + c + }) + // Remove components without `tempN_input` file termal. `Component` doesn't support this kind of sensors yet + .filter(|c| c.input_file.is_some()); + + components.extend(compo); + Some(()) + } + + /// Compute a label out of available information. + /// See the table in `Component::label`'s documentation. + fn format_label(&self, class: &str, id: u32) -> String { + let Component { + device_model, + name, label, - input_file: input_path.to_path_buf(), - max: max.unwrap_or(0.0), - critical, - }; - c.refresh(); - c + .. + } = self; + let has_label = !label.is_empty(); + match (has_label, device_model) { + (true, Some(device_model)) => { + format!("{name} {label} {device_model} {class}{id}") + } + (true, None) => format!("{name} {label}"), + (false, Some(device_model)) => format!("{name} {device_model}"), + (false, None) => format!("{name} {class}{id}"), + } } } impl ComponentExt for Component { fn temperature(&self) -> f32 { - self.temperature + self.temperature.unwrap_or(f32::NAN) } fn max(&self) -> f32 { - self.max + self.max.unwrap_or(f32::NAN) } fn critical(&self) -> Option<f32> { - self.critical + self.threshold_critical } fn label(&self) -> &str { @@ -145,22 +311,28 @@ impl ComponentExt for Component { } fn refresh(&mut self) { - if let Some(content) = get_file_line(self.input_file.as_path(), 10) { - self.temperature = content - .replace('\n', "") - .parse::<f32>() - .unwrap_or(100_000f32) - / 1000f32; - if self.temperature > self.max { - self.max = self.temperature; - } - } + let current = self + .input_file + .as_ref() + .and_then(|file| get_temperature_from_file(file.as_path())); + // tries to read out kernel highest if not compute something from temperature. + let max = self + .highest_file + .as_ref() + .and_then(|file| get_temperature_from_file(file.as_path())) + .or_else(|| { + let last = self.temperature?; + let current = current?; + Some(last.max(current)) + }); + self.max = max; + self.temperature = current; } } pub(crate) fn get_components() -> Vec<Component> { let mut components = Vec::with_capacity(10); - if let Ok(dir) = read_dir(&Path::new("/sys/class/hwmon/")) { + if let Ok(dir) = read_dir(Path::new("/sys/class/hwmon/")) { for entry in dir.flatten() { let entry = entry.path(); if !entry.is_dir() @@ -172,18 +344,9 @@ pub(crate) fn get_components() -> Vec<Component> { { continue; } - append_files(&mut components, &entry); + Component::from_hwmon(&mut components, &entry); } components.sort_by(|c1, c2| c1.label.to_lowercase().cmp(&c2.label.to_lowercase())); } - if is_file("/sys/class/thermal/thermal_zone0/temp") { - // Specfic to raspberry pi. - components.push(Component::new( - "CPU".to_owned(), - Path::new("/sys/class/thermal/thermal_zone0/temp"), - None, - None, - )); - } components } |