sysinfo/unix/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::Component;
8
9use std::collections::HashMap;
10use std::fs::{read_dir, File};
11use std::io::Read;
12use std::path::{Path, PathBuf};
13
14#[derive(Default)]
15pub(crate) struct ComponentInner {
16    /// Optional associated device of a `Component`.
17    device_model: Option<String>,
18    /// The chip name.
19    ///
20    /// Kernel documentation extract:
21    ///
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    ///
54    /// - 1: CPU embedded diode
55    /// - 2: 3904 transistor
56    /// - 3: thermal diode
57    /// - 4: thermistor
58    /// - 5: AMD AMDSI
59    /// - 6: Intel PECI
60    ///
61    /// Not all types are supported by all chips.
62    sensor_type: Option<ThermalSensorType>,
63    /// Component Label
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    // Historical minimum temperature
75    // - Read in:`temp[1-*]_lowest
76    // - Unit: millidegree Celsius
77    //
78    // Temperature critical min value, typically lower than
79    // corresponding temp_min values.
80    // - Read in:`temp[1-*]_lcrit`
81    // - Unit: millidegree Celsius
82    //
83    // Temperature emergency max value, for chips supporting more than
84    // two upper temperature limits. Must be equal or greater than
85    // corresponding temp_crit values.
86    // - temp[1-*]_emergency
87    // - Unit: millidegree Celsius
88    /// File to read current temperature shall be `temp[1-*]_input`
89    /// It may be absent but we don't continue if absent.
90    input_file: Option<PathBuf>,
91    /// `temp[1-*]_highest file` to read if available highest value.
92    highest_file: Option<PathBuf>,
93    pub(crate) updated: bool,
94}
95
96impl ComponentInner {
97    fn update_from(
98        &mut self,
99        Component {
100            inner:
101                ComponentInner {
102                    temperature,
103                    max,
104                    input_file,
105                    highest_file,
106                    ..
107                },
108        }: Component,
109    ) {
110        if let Some(temp) = temperature {
111            self.temperature = Some(temp);
112        }
113        match (max, self.max) {
114            (Some(new_max), Some(old_max)) => self.max = Some(new_max.max(old_max)),
115            (Some(max), None) => self.max = Some(max),
116            _ => {}
117        }
118        if input_file.is_some() && input_file != self.input_file {
119            self.input_file = input_file;
120        }
121        if highest_file.is_some() && highest_file != self.highest_file {
122            self.highest_file = highest_file;
123        }
124        self.updated = true;
125    }
126}
127
128// Read arbitrary data from sysfs.
129fn get_file_line(file: &Path, capacity: usize) -> Option<String> {
130    let mut reader = String::with_capacity(capacity);
131    let mut f = File::open(file).ok()?;
132    f.read_to_string(&mut reader).ok()?;
133    reader.truncate(reader.trim_end().len());
134    Some(reader)
135}
136
137/// Designed at first for reading an `i32` or `u32` aka `c_long`
138/// from a `/sys/class/hwmon` sysfs file.
139fn read_number_from_file<N>(file: &Path) -> Option<N>
140where
141    N: std::str::FromStr,
142{
143    let mut reader = [0u8; 32];
144    let mut f = File::open(file).ok()?;
145    let n = f.read(&mut reader).ok()?;
146    // parse and trim would complain about `\0`.
147    let number = &reader[..n];
148    let number = std::str::from_utf8(number).ok()?;
149    let number = number.trim();
150    // Assert that we cleaned a little bit that string.
151    if cfg!(feature = "debug") {
152        assert!(!number.contains('\n') && !number.contains('\0'));
153    }
154    number.parse().ok()
155}
156
157// Read a temperature from a `tempN_item` sensor form the sysfs.
158// number returned will be in mili-celsius.
159//
160// Don't call it on `label`, `name` or `type` file.
161#[inline]
162fn get_temperature_from_file(file: &Path) -> Option<f32> {
163    let temp = read_number_from_file(file);
164    convert_temp_celsius(temp)
165}
166
167/// Takes a raw temperature in mili-celsius and convert it to celsius.
168#[inline]
169fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
170    temp.map(|n| (n as f32) / 1000f32)
171}
172
173/// Information about thermal sensor. It may be unavailable as it's
174/// kernel module and chip dependent.
175enum ThermalSensorType {
176    /// 1: CPU embedded diode
177    CPUEmbeddedDiode,
178    /// 2: 3904 transistor
179    Transistor3904,
180    /// 3: thermal diode
181    ThermalDiode,
182    /// 4: thermistor
183    Thermistor,
184    /// 5: AMD AMDSI
185    AMDAMDSI,
186    /// 6: Intel PECI
187    IntelPECI,
188    /// Not all types are supported by all chips so we keep space for unknown sensors.
189    #[allow(dead_code)]
190    Unknown(u8),
191}
192
193impl From<u8> for ThermalSensorType {
194    fn from(input: u8) -> Self {
195        match input {
196            0 => Self::CPUEmbeddedDiode,
197            1 => Self::Transistor3904,
198            3 => Self::ThermalDiode,
199            4 => Self::Thermistor,
200            5 => Self::AMDAMDSI,
201            6 => Self::IntelPECI,
202            n => Self::Unknown(n),
203        }
204    }
205}
206
207/// Check given `item` dispatch to read the right `file` with the right parsing and store data in
208/// given `component`. `id` is provided for `label` creation.
209fn fill_component(component: &mut ComponentInner, item: &str, folder: &Path, file: &str) {
210    let hwmon_file = folder.join(file);
211    match item {
212        "type" => {
213            component.sensor_type =
214                read_number_from_file::<u8>(&hwmon_file).map(ThermalSensorType::from)
215        }
216        "input" => {
217            let temperature = get_temperature_from_file(&hwmon_file);
218            component.input_file = Some(hwmon_file);
219            component.temperature = temperature;
220            // Maximum know try to get it from `highest` if not available
221            // use current temperature
222            if component.max.is_none() {
223                component.max = temperature;
224            }
225        }
226        "label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(),
227        "highest" => {
228            component.max = get_temperature_from_file(&hwmon_file).or(component.temperature);
229            component.highest_file = Some(hwmon_file);
230        }
231        // "max" => component.threshold_max = get_temperature_from_file(&hwmon_file),
232        // "min" => component.threshold_min = get_temperature_from_file(&hwmon_file),
233        "crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file),
234        _ => {
235            sysinfo_debug!(
236                "This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}",
237                hwmon_file,
238            );
239        }
240    }
241}
242
243impl ComponentInner {
244    /// Read out `hwmon` info (hardware monitor) from `folder`
245    /// to get values' path to be used on refresh as well as files containing `max`,
246    /// `critical value` and `label`. Then we store everything into `components`.
247    ///
248    /// Note that a thermal [Component] must have a way to read its temperature.
249    /// If not, it will be ignored and not added into `components`.
250    ///
251    /// ## What is read:
252    ///
253    /// - Mandatory: `name` the name of the `hwmon`.
254    /// - Mandatory: `tempN_input` Drop [Component] if missing
255    /// - Optional: sensor `label`, in the general case content of `tempN_label`
256    ///   see below for special cases
257    /// - Optional: `label`
258    /// - Optional: `/device/model`
259    /// - Optional: highest historic value in `tempN_highest`.
260    /// - Optional: max threshold value defined in `tempN_max`
261    /// - Optional: critical threshold value defined in `tempN_crit`
262    ///
263    /// Where `N` is a `u32` associated to a sensor like `temp1_max`, `temp1_input`.
264    ///
265    /// ## Doc to Linux kernel API.
266    ///
267    /// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html
268    /// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces
269    /// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html
270    fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> {
271        let dir = read_dir(folder).ok()?;
272        let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10);
273        for entry in dir.flatten() {
274            if !entry.file_type().is_ok_and(|file_type| !file_type.is_dir()) {
275                continue;
276            }
277
278            let entry = entry.path();
279            let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("");
280            let Some((id, item)) = filename
281                .strip_prefix("temp")
282                .and_then(|f| f.split_once('_'))
283                .and_then(|(id, item)| Some((id.parse::<u32>().ok()?, item)))
284            else {
285                continue;
286            };
287
288            let component = matchings.entry(id).or_insert_with(|| Component {
289                inner: ComponentInner::default(),
290            });
291            let component = &mut component.inner;
292            let name = get_file_line(&folder.join("name"), 16);
293            component.name = name.unwrap_or_default();
294            let device_model = get_file_line(&folder.join("device/model"), 16);
295            component.device_model = device_model;
296            fill_component(component, item, folder, filename);
297        }
298        for (id, mut new_comp) in matchings
299            .into_iter()
300            // Remove components without `tempN_input` file termal. `Component` doesn't support this
301            // kind of sensors yet
302            .filter(|(_, c)| c.inner.input_file.is_some())
303        {
304            // compute label from known data
305            new_comp.inner.label = new_comp.inner.format_label("temp", id);
306            if let Some(comp) = components
307                .iter_mut()
308                .find(|comp| comp.inner.label == new_comp.inner.label)
309            {
310                comp.inner.update_from(new_comp);
311            } else {
312                new_comp.inner.updated = true;
313                components.push(new_comp);
314            }
315        }
316
317        Some(())
318    }
319
320    /// Compute a label out of available information.
321    /// See the table in `Component::label`'s documentation.
322    fn format_label(&self, class: &str, id: u32) -> String {
323        let ComponentInner {
324            device_model,
325            name,
326            label,
327            ..
328        } = self;
329        let has_label = !label.is_empty();
330        match (has_label, device_model) {
331            (true, Some(device_model)) => {
332                format!("{name} {label} {device_model}")
333            }
334            (true, None) => format!("{name} {label}"),
335            (false, Some(device_model)) => format!("{name} {device_model}"),
336            (false, None) => format!("{name} {class}{id}"),
337        }
338    }
339
340    pub(crate) fn temperature(&self) -> Option<f32> {
341        self.temperature
342    }
343
344    pub(crate) fn max(&self) -> Option<f32> {
345        self.max
346    }
347
348    pub(crate) fn critical(&self) -> Option<f32> {
349        self.threshold_critical
350    }
351
352    pub(crate) fn label(&self) -> &str {
353        &self.label
354    }
355
356    pub(crate) fn refresh(&mut self) {
357        let current = self
358            .input_file
359            .as_ref()
360            .and_then(|file| get_temperature_from_file(file.as_path()));
361        // tries to read out kernel highest if not compute something from temperature.
362        let max = self
363            .highest_file
364            .as_ref()
365            .and_then(|file| get_temperature_from_file(file.as_path()))
366            .or_else(|| {
367                let last = self.temperature?;
368                let current = current?;
369                Some(last.max(current))
370            });
371        self.max = max;
372        self.temperature = current;
373    }
374}
375
376fn read_temp_dir<F: FnMut(PathBuf)>(path: &str, starts_with: &str, mut f: F) {
377    if let Ok(dir) = read_dir(path) {
378        for entry in dir.flatten() {
379            if !entry
380                .file_name()
381                .to_str()
382                .unwrap_or("")
383                .starts_with(starts_with)
384            {
385                continue;
386            }
387            let path = entry.path();
388            if !path.is_file() {
389                f(path);
390            }
391        }
392    }
393}
394
395pub(crate) struct ComponentsInner {
396    pub(crate) components: Vec<Component>,
397}
398
399impl ComponentsInner {
400    pub(crate) fn new() -> Self {
401        Self {
402            components: Vec::with_capacity(4),
403        }
404    }
405
406    pub(crate) fn from_vec(components: Vec<Component>) -> Self {
407        Self { components }
408    }
409
410    pub(crate) fn into_vec(self) -> Vec<Component> {
411        self.components
412    }
413
414    pub(crate) fn list(&self) -> &[Component] {
415        &self.components
416    }
417
418    pub(crate) fn list_mut(&mut self) -> &mut [Component] {
419        &mut self.components
420    }
421
422    pub(crate) fn refresh(&mut self) {
423        self.refresh_from_sys_class_path("/sys/class");
424    }
425
426    fn refresh_from_sys_class_path(&mut self, path: &str) {
427        let hwmon_path = format!("{path}/hwmon");
428        read_temp_dir(&hwmon_path, "hwmon", |path| {
429            ComponentInner::from_hwmon(&mut self.components, &path);
430        });
431        if self.components.is_empty() {
432            // Normally should only be used by raspberry pi.
433            let thermal_path = format!("{path}/thermal");
434            read_temp_dir(&thermal_path, "thermal_", |path| {
435                let temp = path.join("temp");
436                if temp.exists() {
437                    let Some(name) = get_file_line(&path.join("type"), 16) else {
438                        return;
439                    };
440                    let mut component = ComponentInner {
441                        name,
442                        ..Default::default()
443                    };
444                    fill_component(&mut component, "input", &path, "temp");
445                    self.components.push(Component { inner: component });
446                }
447            });
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455    use std::fs;
456    use tempfile;
457
458    #[test]
459    fn test_component_refresh_simple() {
460        let temp_dir = tempfile::tempdir().expect("failed to create temporary directory");
461        let hwmon0_dir = temp_dir.path().join("hwmon/hwmon0");
462
463        fs::create_dir_all(temp_dir.path().join("hwmon/hwmon0"))
464            .expect("failed to create hwmon/hwmon0 directory");
465
466        fs::write(hwmon0_dir.join("name"), "test_name").expect("failed to write to name file");
467        fs::write(hwmon0_dir.join("temp1_input"), "1234")
468            .expect("failed to write to temp1_input file");
469
470        let mut components = ComponentsInner::new();
471        components.refresh_from_sys_class_path(
472            temp_dir
473                .path()
474                .to_str()
475                .expect("failed to convert path to string"),
476        );
477        let components = components.into_vec();
478
479        assert_eq!(components.len(), 1);
480        assert_eq!(components[0].inner.name, "test_name");
481        assert_eq!(components[0].label(), "test_name temp1");
482        assert_eq!(components[0].temperature(), Some(1.234));
483    }
484
485    #[test]
486    fn test_component_refresh_with_more_data() {
487        let temp_dir = tempfile::tempdir().expect("failed to create temporary directory");
488        let hwmon0_dir = temp_dir.path().join("hwmon/hwmon0");
489
490        // create hwmon0 file including device/model file
491        fs::create_dir_all(hwmon0_dir.join("device"))
492            .expect("failed to create hwmon/hwmon0 directory");
493
494        fs::write(hwmon0_dir.join("name"), "test_name").expect("failed to write to name file");
495        fs::write(hwmon0_dir.join("device/model"), "test_model")
496            .expect("failed to write to model file");
497
498        fs::write(hwmon0_dir.join("temp1_label"), "test_label1")
499            .expect("failed to write to temp1_label file");
500        fs::write(hwmon0_dir.join("temp1_input"), "1234")
501            .expect("failed to write to temp1_input file");
502        fs::write(hwmon0_dir.join("temp1_crit"), "100").expect("failed to write to temp1_min file");
503
504        let mut components = ComponentsInner::new();
505        components.refresh_from_sys_class_path(
506            temp_dir
507                .path()
508                .to_str()
509                .expect("failed to convert path to string"),
510        );
511        let components = components.into_vec();
512
513        assert_eq!(components.len(), 1);
514        assert_eq!(components[0].inner.name, "test_name");
515        assert_eq!(components[0].label(), "test_name test_label1 test_model");
516        assert_eq!(components[0].temperature(), Some(1.234));
517        assert_eq!(components[0].max(), Some(1.234));
518        assert_eq!(components[0].critical(), Some(0.1));
519    }
520
521    #[test]
522    fn test_component_refresh_multiple_sensors() {
523        let temp_dir = tempfile::tempdir().expect("failed to create temporary directory");
524        let hwmon0_dir = temp_dir.path().join("hwmon/hwmon0");
525        fs::create_dir_all(&hwmon0_dir).expect("failed to create hwmon/hwmon0 directory");
526
527        fs::write(hwmon0_dir.join("name"), "test_name").expect("failed to write to name file");
528
529        fs::write(hwmon0_dir.join("temp1_label"), "test_label1")
530            .expect("failed to write to temp1_label file");
531        fs::write(hwmon0_dir.join("temp1_input"), "1234")
532            .expect("failed to write to temp1_input file");
533        fs::write(hwmon0_dir.join("temp1_crit"), "100").expect("failed to write to temp1_min file");
534
535        fs::write(hwmon0_dir.join("temp2_label"), "test_label2")
536            .expect("failed to write to temp2_label file");
537        fs::write(hwmon0_dir.join("temp2_input"), "5678")
538            .expect("failed to write to temp2_input file");
539        fs::write(hwmon0_dir.join("temp2_crit"), "200").expect("failed to write to temp2_min file");
540
541        let mut components = ComponentsInner::new();
542        components.refresh_from_sys_class_path(
543            temp_dir
544                .path()
545                .to_str()
546                .expect("failed to convert path to string"),
547        );
548        let mut components = components.into_vec();
549        components.sort_by_key(|c| c.inner.label.clone());
550
551        assert_eq!(components.len(), 2);
552        assert_eq!(components[0].inner.name, "test_name");
553        assert_eq!(components[0].label(), "test_name test_label1");
554        assert_eq!(components[0].temperature(), Some(1.234));
555        assert_eq!(components[0].max(), Some(1.234));
556        assert_eq!(components[0].critical(), Some(0.1));
557
558        assert_eq!(components[1].inner.name, "test_name");
559        assert_eq!(components[1].label(), "test_name test_label2");
560        assert_eq!(components[1].temperature(), Some(5.678));
561        assert_eq!(components[1].max(), Some(5.678));
562        assert_eq!(components[1].critical(), Some(0.2));
563    }
564
565    #[test]
566    fn test_component_refresh_multiple_sensors_with_device_model() {
567        let temp_dir = tempfile::tempdir().expect("failed to create temporary directory");
568        let hwmon0_dir = temp_dir.path().join("hwmon/hwmon0");
569
570        // create hwmon0 file including device/model file
571        fs::create_dir_all(hwmon0_dir.join("device"))
572            .expect("failed to create hwmon/hwmon0 directory");
573
574        fs::write(hwmon0_dir.join("name"), "test_name").expect("failed to write to name file");
575        fs::write(hwmon0_dir.join("device/model"), "test_model")
576            .expect("failed to write to model file");
577
578        fs::write(hwmon0_dir.join("temp1_label"), "test_label1")
579            .expect("failed to write to temp1_label file");
580        fs::write(hwmon0_dir.join("temp1_input"), "1234")
581            .expect("failed to write to temp1_input file");
582        fs::write(hwmon0_dir.join("temp1_crit"), "100").expect("failed to write to temp1_min file");
583
584        fs::write(hwmon0_dir.join("temp2_label"), "test_label2")
585            .expect("failed to write to temp2_label file");
586        fs::write(hwmon0_dir.join("temp2_input"), "5678")
587            .expect("failed to write to temp2_input file");
588        fs::write(hwmon0_dir.join("temp2_crit"), "200").expect("failed to write to temp2_min file");
589
590        let mut components = ComponentsInner::new();
591        components.refresh_from_sys_class_path(
592            temp_dir
593                .path()
594                .to_str()
595                .expect("failed to convert path to string"),
596        );
597        let mut components = components.into_vec();
598        components.sort_by_key(|c| c.inner.label.clone());
599
600        assert_eq!(components.len(), 2);
601        assert_eq!(components[0].inner.name, "test_name");
602        assert_eq!(components[0].label(), "test_name test_label1 test_model");
603        assert_eq!(components[0].temperature(), Some(1.234));
604        assert_eq!(components[0].max(), Some(1.234));
605        assert_eq!(components[0].critical(), Some(0.1));
606
607        assert_eq!(components[1].inner.name, "test_name");
608        assert_eq!(components[1].label(), "test_name test_label2 test_model");
609        assert_eq!(components[1].temperature(), Some(5.678));
610        assert_eq!(components[1].max(), Some(5.678));
611        assert_eq!(components[1].critical(), Some(0.2));
612    }
613}