sysinfo/linux/
component.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3// Information about values readable from `hwmon` sysfs.
4//
5// Values in /sys/class/hwmonN are `c_long` or `c_ulong`
6// transposed to rust we only read `u32` or `i32` values.
7use crate::ComponentExt;
8
9use std::collections::HashMap;
10use std::fs::{read_dir, File};
11use std::io::Read;
12use std::path::{Path, PathBuf};
13
14#[doc = include_str!("../../md_doc/component.md")]
15#[derive(Default)]
16pub struct Component {
17    /// Optional associated device of a `Component`.
18    device_model: Option<String>,
19    /// The chip name.
20    ///
21    /// Kernel documentation extract:
22    /// ```txt
23    /// This should be a short, lowercase string, not containing
24    /// whitespace, dashes, or the wildcard character '*'.
25    /// This attribute represents the chip name. It is the only
26    /// mandatory attribute.
27    /// I2C devices get this attribute created automatically.
28    /// ```
29    name: String,
30    /// Temperature current value
31    /// - Read in: `temp[1-*]_input`.
32    /// - Unit: read as millidegree Celsius converted to Celsius.
33    temperature: Option<f32>,
34    /// Maximum value computed by `sysinfo`.
35    max: Option<f32>,
36    /// Max threshold provided by the chip/kernel
37    /// - Read in:`temp[1-*]_max`
38    /// - Unit: read as millidegree Celsius converted to Celsius.
39    threshold_max: Option<f32>,
40    /// Min threshold provided by the chip/kernel.
41    /// - Read in:`temp[1-*]_min`
42    /// - Unit: read as millidegree Celsius converted to Celsius.
43    threshold_min: Option<f32>,
44    /// Critical threshold provided by the chip/kernel previous user write.
45    /// Read in `temp[1-*]_crit`:
46    /// Typically greater than corresponding temp_max values.
47    /// - Unit: read as millidegree Celsius converted to Celsius.
48    threshold_critical: Option<f32>,
49    /// Sensor type, not common but can exist!
50    ///
51    /// Read in: `temp[1-*]_type` Sensor type selection.
52    /// Values integer:
53    /// - 1: CPU embedded diode
54    /// - 2: 3904 transistor
55    /// - 3: thermal diode
56    /// - 4: thermistor
57    /// - 5: AMD AMDSI
58    /// - 6: Intel PECI
59    /// Not all types are supported by all chips
60    sensor_type: Option<TermalSensorType>,
61    /// Component Label
62    ///
63    /// For formatting detail see `Component::label` function docstring.
64    ///
65    /// ## Linux implementation details
66    ///
67    /// read n: `temp[1-*]_label` Suggested temperature channel label.
68    /// Value: Text string
69    ///
70    /// Should only be created if the driver has hints about what
71    /// this temperature channel is being used for, and user-space
72    /// doesn't. In all other cases, the label is provided by user-space.
73    label: String,
74    // TODO: not used now.
75    // Historical minimum temperature
76    // - Read in:`temp[1-*]_lowest
77    // - Unit: millidegree Celsius
78    //
79    // Temperature critical min value, typically lower than
80    // corresponding temp_min values.
81    // - Read in:`temp[1-*]_lcrit`
82    // - Unit: millidegree Celsius
83    //
84    // Temperature emergency max value, for chips supporting more than
85    // two upper temperature limits. Must be equal or greater than
86    // corresponding temp_crit values.
87    // - temp[1-*]_emergency
88    // - Unit: millidegree Celsius
89    /// File to read current temperature shall be `temp[1-*]_input`
90    /// It may be absent but we don't continue if absent.
91    input_file: Option<PathBuf>,
92    /// `temp[1-*]_highest file` to read if available highest value.
93    highest_file: Option<PathBuf>,
94}
95
96// Read arbitrary data from sysfs.
97fn get_file_line(file: &Path, capacity: usize) -> Option<String> {
98    let mut reader = String::with_capacity(capacity);
99    let mut f = File::open(file).ok()?;
100    f.read_to_string(&mut reader).ok()?;
101    reader.truncate(reader.trim_end().len());
102    Some(reader)
103}
104
105/// Designed at first for reading an `i32` or `u32` aka `c_long`
106/// from a `/sys/class/hwmon` sysfs file.
107fn read_number_from_file<N>(file: &Path) -> Option<N>
108where
109    N: std::str::FromStr,
110{
111    let mut reader = [0u8; 32];
112    let mut f = File::open(file).ok()?;
113    let n = f.read(&mut reader).ok()?;
114    // parse and trim would complain about `\0`.
115    let number = &reader[..n];
116    let number = std::str::from_utf8(number).ok()?;
117    let number = number.trim();
118    // Assert that we cleaned a little bit that string.
119    if cfg!(feature = "debug") {
120        assert!(!number.contains('\n') && !number.contains('\0'));
121    }
122    number.parse().ok()
123}
124
125// Read a temperature from a `tempN_item` sensor form the sysfs.
126// number returned will be in mili-celsius.
127//
128// Don't call it on `label`, `name` or `type` file.
129#[inline]
130fn get_temperature_from_file(file: &Path) -> Option<f32> {
131    let temp = read_number_from_file(file);
132    convert_temp_celsius(temp)
133}
134
135/// Takes a raw temperature in mili-celsius and convert it to celsius.
136#[inline]
137fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
138    temp.map(|n| (n as f32) / 1000f32)
139}
140
141/// Information about thermal sensor. It may be unavailable as it's
142/// kernel module and chip dependent.
143enum TermalSensorType {
144    /// 1: CPU embedded diode
145    CPUEmbeddedDiode,
146    /// 2: 3904 transistor
147    Transistor3904,
148    /// 3: thermal diode
149    ThermalDiode,
150    /// 4: thermistor
151    Thermistor,
152    /// 5: AMD AMDSI
153    AMDAMDSI,
154    /// 6: Intel PECI
155    IntelPECI,
156    /// Not all types are supported by all chips so we keep space for
157    /// unknown sensors.
158    Unknown(u8),
159}
160
161impl From<u8> for TermalSensorType {
162    fn from(input: u8) -> Self {
163        match input {
164            0 => Self::CPUEmbeddedDiode,
165            1 => Self::Transistor3904,
166            3 => Self::ThermalDiode,
167            4 => Self::Thermistor,
168            5 => Self::AMDAMDSI,
169            6 => Self::IntelPECI,
170            n => Self::Unknown(n),
171        }
172    }
173}
174
175/// Check given `item` dispatch to read the right `file` with the right parsing and store data in
176/// given `component`. `id` is provided for `label` creation.
177fn fill_component(component: &mut Component, item: &str, folder: &Path, file: &str) {
178    let hwmon_file = folder.join(file);
179    match item {
180        "type" => {
181            component.sensor_type =
182                read_number_from_file::<u8>(&hwmon_file).map(TermalSensorType::from)
183        }
184        "input" => {
185            let temperature = get_temperature_from_file(&hwmon_file);
186            component.input_file = Some(hwmon_file);
187            component.temperature = temperature;
188            // Maximum know try to get it from `highest` if not available
189            // use current temperature
190            if component.max.is_none() {
191                component.max = temperature;
192            }
193        }
194        "label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(),
195        "highest" => {
196            component.max = get_temperature_from_file(&hwmon_file).or(component.temperature);
197            component.highest_file = Some(hwmon_file);
198        }
199        "max" => component.threshold_max = get_temperature_from_file(&hwmon_file),
200        "min" => component.threshold_min = get_temperature_from_file(&hwmon_file),
201        "crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file),
202        _ => {
203            sysinfo_debug!(
204                "This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}",
205                hwmon_file,
206            );
207        }
208    }
209}
210
211impl Component {
212    /// Read out `hwmon` info (hardware monitor) from `folder`
213    /// to get values' path to be used on refresh as well as files containing `max`,
214    /// `critical value` and `label`. Then we store everything into `components`.
215    ///
216    /// Note that a thermal [Component] must have a way to read its temperature.
217    /// If not, it will be ignored and not added into `components`.
218    ///
219    /// ## What is read:
220    ///
221    /// - Mandatory: `name` the name of the `hwmon`.
222    /// - Mandatory: `tempN_input` Drop [Component] if missing
223    /// - Optional: sensor `label`, in the general case content of `tempN_label`
224    ///   see below for special cases
225    /// - Optional: `label`
226    /// - Optional: `/device/model`
227    /// - Optional: hightest historic value in `tempN_hightest`.
228    /// - Optional: max threshold value defined in `tempN_max`
229    /// - Optional: critical threshold value defined in `tempN_crit`
230    ///
231    /// Where `N` is a `u32` associated to a sensor like `temp1_max`, `temp1_input`.
232    ///
233    /// ## Doc to Linux kernel API.
234    ///
235    /// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html
236    /// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces
237    /// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html
238    fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> {
239        let dir = read_dir(folder).ok()?;
240        let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10);
241        for entry in dir.flatten() {
242            let entry = entry.path();
243            let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("");
244            if entry.is_dir() || !filename.starts_with("temp") {
245                continue;
246            }
247
248            let (id, item) = filename.split_once('_')?;
249            let id = id.get(4..)?.parse::<u32>().ok()?;
250
251            let component = matchings.entry(id).or_default();
252            let name = get_file_line(&folder.join("name"), 16);
253            component.name = name.unwrap_or_default();
254            let device_model = get_file_line(&folder.join("device/model"), 16);
255            component.device_model = device_model;
256            fill_component(component, item, folder, filename);
257        }
258        let compo = matchings
259            .into_iter()
260            .map(|(id, mut c)| {
261                // sysinfo expose a generic interface with a `label`.
262                // Problem: a lot of sensors don't have a label or a device model! ¯\_(ツ)_/¯
263                // So let's pretend we have a unique label!
264                // See the table in `Component::label` documentation for the table detail.
265                c.label = c.format_label("temp", id);
266                c
267            })
268            // Remove components without `tempN_input` file termal. `Component` doesn't support this kind of sensors yet
269            .filter(|c| c.input_file.is_some());
270
271        components.extend(compo);
272        Some(())
273    }
274
275    /// Compute a label out of available information.
276    /// See the table in `Component::label`'s documentation.
277    fn format_label(&self, class: &str, id: u32) -> String {
278        let Component {
279            device_model,
280            name,
281            label,
282            ..
283        } = self;
284        let has_label = !label.is_empty();
285        match (has_label, device_model) {
286            (true, Some(device_model)) => {
287                format!("{name} {label} {device_model} {class}{id}")
288            }
289            (true, None) => format!("{name} {label}"),
290            (false, Some(device_model)) => format!("{name} {device_model}"),
291            (false, None) => format!("{name} {class}{id}"),
292        }
293    }
294}
295
296impl ComponentExt for Component {
297    fn temperature(&self) -> f32 {
298        self.temperature.unwrap_or(f32::NAN)
299    }
300
301    fn max(&self) -> f32 {
302        self.max.unwrap_or(f32::NAN)
303    }
304
305    fn critical(&self) -> Option<f32> {
306        self.threshold_critical
307    }
308
309    fn label(&self) -> &str {
310        &self.label
311    }
312
313    fn refresh(&mut self) {
314        let current = self
315            .input_file
316            .as_ref()
317            .and_then(|file| get_temperature_from_file(file.as_path()));
318        // tries to read out kernel highest if not compute something from temperature.
319        let max = self
320            .highest_file
321            .as_ref()
322            .and_then(|file| get_temperature_from_file(file.as_path()))
323            .or_else(|| {
324                let last = self.temperature?;
325                let current = current?;
326                Some(last.max(current))
327            });
328        self.max = max;
329        self.temperature = current;
330    }
331}
332
333pub(crate) fn get_components() -> Vec<Component> {
334    let mut components = Vec::with_capacity(10);
335    if let Ok(dir) = read_dir(Path::new("/sys/class/hwmon/")) {
336        for entry in dir.flatten() {
337            let entry = entry.path();
338            if !entry.is_dir()
339                || !entry
340                    .file_name()
341                    .and_then(|x| x.to_str())
342                    .unwrap_or("")
343                    .starts_with("hwmon")
344            {
345                continue;
346            }
347            Component::from_hwmon(&mut components, &entry);
348        }
349        components.sort_by(|c1, c2| c1.label.to_lowercase().cmp(&c2.label.to_lowercase()));
350    }
351    components
352}