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    /// For formatting detail see `Component::label` function docstring.
66    ///
67    /// ## Linux implementation details
68    ///
69    /// read n: `temp[1-*]_label` Suggested temperature channel label.
70    /// Value: Text string
71    ///
72    /// Should only be created if the driver has hints about what
73    /// this temperature channel is being used for, and user-space
74    /// doesn't. In all other cases, the label is provided by user-space.
75    label: String,
76    // TODO: not used now.
77    // Historical minimum temperature
78    // - Read in:`temp[1-*]_lowest
79    // - Unit: millidegree Celsius
80    //
81    // Temperature critical min value, typically lower than
82    // corresponding temp_min values.
83    // - Read in:`temp[1-*]_lcrit`
84    // - Unit: millidegree Celsius
85    //
86    // Temperature emergency max value, for chips supporting more than
87    // two upper temperature limits. Must be equal or greater than
88    // corresponding temp_crit values.
89    // - temp[1-*]_emergency
90    // - Unit: millidegree Celsius
91    /// File to read current temperature shall be `temp[1-*]_input`
92    /// It may be absent but we don't continue if absent.
93    input_file: Option<PathBuf>,
94    /// `temp[1-*]_highest file` to read if available highest value.
95    highest_file: Option<PathBuf>,
96}
97
98// Read arbitrary data from sysfs.
99fn get_file_line(file: &Path, capacity: usize) -> Option<String> {
100    let mut reader = String::with_capacity(capacity);
101    let mut f = File::open(file).ok()?;
102    f.read_to_string(&mut reader).ok()?;
103    reader.truncate(reader.trim_end().len());
104    Some(reader)
105}
106
107/// Designed at first for reading an `i32` or `u32` aka `c_long`
108/// from a `/sys/class/hwmon` sysfs file.
109fn read_number_from_file<N>(file: &Path) -> Option<N>
110where
111    N: std::str::FromStr,
112{
113    let mut reader = [0u8; 32];
114    let mut f = File::open(file).ok()?;
115    let n = f.read(&mut reader).ok()?;
116    // parse and trim would complain about `\0`.
117    let number = &reader[..n];
118    let number = std::str::from_utf8(number).ok()?;
119    let number = number.trim();
120    // Assert that we cleaned a little bit that string.
121    if cfg!(feature = "debug") {
122        assert!(!number.contains('\n') && !number.contains('\0'));
123    }
124    number.parse().ok()
125}
126
127// Read a temperature from a `tempN_item` sensor form the sysfs.
128// number returned will be in mili-celsius.
129//
130// Don't call it on `label`, `name` or `type` file.
131#[inline]
132fn get_temperature_from_file(file: &Path) -> Option<f32> {
133    let temp = read_number_from_file(file);
134    convert_temp_celsius(temp)
135}
136
137/// Takes a raw temperature in mili-celsius and convert it to celsius.
138#[inline]
139fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
140    temp.map(|n| (n as f32) / 1000f32)
141}
142
143/// Information about thermal sensor. It may be unavailable as it's
144/// kernel module and chip dependent.
145enum ThermalSensorType {
146    /// 1: CPU embedded diode
147    CPUEmbeddedDiode,
148    /// 2: 3904 transistor
149    Transistor3904,
150    /// 3: thermal diode
151    ThermalDiode,
152    /// 4: thermistor
153    Thermistor,
154    /// 5: AMD AMDSI
155    AMDAMDSI,
156    /// 6: Intel PECI
157    IntelPECI,
158    /// Not all types are supported by all chips so we keep space for
159    /// unknown sensors.
160    #[allow(dead_code)]
161    Unknown(u8),
162}
163
164impl From<u8> for ThermalSensorType {
165    fn from(input: u8) -> Self {
166        match input {
167            0 => Self::CPUEmbeddedDiode,
168            1 => Self::Transistor3904,
169            3 => Self::ThermalDiode,
170            4 => Self::Thermistor,
171            5 => Self::AMDAMDSI,
172            6 => Self::IntelPECI,
173            n => Self::Unknown(n),
174        }
175    }
176}
177
178/// Check given `item` dispatch to read the right `file` with the right parsing and store data in
179/// given `component`. `id` is provided for `label` creation.
180fn fill_component(component: &mut ComponentInner, item: &str, folder: &Path, file: &str) {
181    let hwmon_file = folder.join(file);
182    match item {
183        "type" => {
184            component.sensor_type =
185                read_number_from_file::<u8>(&hwmon_file).map(ThermalSensorType::from)
186        }
187        "input" => {
188            let temperature = get_temperature_from_file(&hwmon_file);
189            component.input_file = Some(hwmon_file);
190            component.temperature = temperature;
191            // Maximum know try to get it from `highest` if not available
192            // use current temperature
193            if component.max.is_none() {
194                component.max = temperature;
195            }
196        }
197        "label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(),
198        "highest" => {
199            component.max = get_temperature_from_file(&hwmon_file).or(component.temperature);
200            component.highest_file = Some(hwmon_file);
201        }
202        "max" => component.threshold_max = get_temperature_from_file(&hwmon_file),
203        "min" => component.threshold_min = get_temperature_from_file(&hwmon_file),
204        "crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file),
205        _ => {
206            sysinfo_debug!(
207                "This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}",
208                hwmon_file,
209            );
210        }
211    }
212}
213
214impl ComponentInner {
215    /// Read out `hwmon` info (hardware monitor) from `folder`
216    /// to get values' path to be used on refresh as well as files containing `max`,
217    /// `critical value` and `label`. Then we store everything into `components`.
218    ///
219    /// Note that a thermal [Component] must have a way to read its temperature.
220    /// If not, it will be ignored and not added into `components`.
221    ///
222    /// ## What is read:
223    ///
224    /// - Mandatory: `name` the name of the `hwmon`.
225    /// - Mandatory: `tempN_input` Drop [Component] if missing
226    /// - Optional: sensor `label`, in the general case content of `tempN_label`
227    ///   see below for special cases
228    /// - Optional: `label`
229    /// - Optional: `/device/model`
230    /// - Optional: highest historic value in `tempN_highest`.
231    /// - Optional: max threshold value defined in `tempN_max`
232    /// - Optional: critical threshold value defined in `tempN_crit`
233    ///
234    /// Where `N` is a `u32` associated to a sensor like `temp1_max`, `temp1_input`.
235    ///
236    /// ## Doc to Linux kernel API.
237    ///
238    /// Kernel hwmon API: https://www.kernel.org/doc/html/latest/hwmon/hwmon-kernel-api.html
239    /// DriveTemp kernel API: https://docs.kernel.org/gpu/amdgpu/thermal.html#hwmon-interfaces
240    /// Amdgpu hwmon interface: https://www.kernel.org/doc/html/latest/hwmon/drivetemp.html
241    fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> {
242        let dir = read_dir(folder).ok()?;
243        let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10);
244        for entry in dir.flatten() {
245            let Ok(file_type) = entry.file_type() else {
246                continue;
247            };
248            if file_type.is_dir() {
249                continue;
250            }
251
252            let entry = entry.path();
253            let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("");
254            if !filename.starts_with("temp") {
255                continue;
256            }
257
258            let (id, item) = filename.split_once('_')?;
259            let id = id.get(4..)?.parse::<u32>().ok()?;
260
261            let component = matchings.entry(id).or_insert_with(|| Component {
262                inner: ComponentInner::default(),
263            });
264            let component = &mut component.inner;
265            let name = get_file_line(&folder.join("name"), 16);
266            component.name = name.unwrap_or_default();
267            let device_model = get_file_line(&folder.join("device/model"), 16);
268            component.device_model = device_model;
269            fill_component(component, item, folder, filename);
270        }
271        let compo = matchings
272            .into_iter()
273            .map(|(id, mut c)| {
274                // sysinfo expose a generic interface with a `label`.
275                // Problem: a lot of sensors don't have a label or a device model! ¯\_(ツ)_/¯
276                // So let's pretend we have a unique label!
277                // See the table in `Component::label` documentation for the table detail.
278                c.inner.label = c.inner.format_label("temp", id);
279                c
280            })
281            // Remove components without `tempN_input` file thermal. `Component` doesn't support
282            // this kind of sensors yet
283            .filter(|c| c.inner.input_file.is_some());
284
285        components.extend(compo);
286        Some(())
287    }
288
289    /// Compute a label out of available information.
290    /// See the table in `Component::label`'s documentation.
291    fn format_label(&self, class: &str, id: u32) -> String {
292        let ComponentInner {
293            device_model,
294            name,
295            label,
296            ..
297        } = self;
298        let has_label = !label.is_empty();
299        match (has_label, device_model) {
300            (true, Some(device_model)) => {
301                format!("{name} {label} {device_model} {class}{id}")
302            }
303            (true, None) => format!("{name} {label}"),
304            (false, Some(device_model)) => format!("{name} {device_model}"),
305            (false, None) => format!("{name} {class}{id}"),
306        }
307    }
308
309    pub(crate) fn temperature(&self) -> f32 {
310        self.temperature.unwrap_or(f32::NAN)
311    }
312
313    pub(crate) fn max(&self) -> f32 {
314        self.max.unwrap_or(f32::NAN)
315    }
316
317    pub(crate) fn critical(&self) -> Option<f32> {
318        self.threshold_critical
319    }
320
321    pub(crate) fn label(&self) -> &str {
322        &self.label
323    }
324
325    pub(crate) fn refresh(&mut self) {
326        let current = self
327            .input_file
328            .as_ref()
329            .and_then(|file| get_temperature_from_file(file.as_path()));
330        // tries to read out kernel highest if not compute something from temperature.
331        let max = self
332            .highest_file
333            .as_ref()
334            .and_then(|file| get_temperature_from_file(file.as_path()))
335            .or_else(|| {
336                let last = self.temperature?;
337                let current = current?;
338                Some(last.max(current))
339            });
340        self.max = max;
341        self.temperature = current;
342    }
343}
344
345pub(crate) struct ComponentsInner {
346    components: Vec<Component>,
347}
348
349impl ComponentsInner {
350    pub(crate) fn new() -> Self {
351        Self {
352            components: Vec::with_capacity(4),
353        }
354    }
355
356    pub(crate) fn from_vec(components: Vec<Component>) -> Self {
357        Self { components }
358    }
359
360    pub(crate) fn into_vec(self) -> Vec<Component> {
361        self.components
362    }
363
364    pub(crate) fn list(&self) -> &[Component] {
365        &self.components
366    }
367
368    pub(crate) fn list_mut(&mut self) -> &mut [Component] {
369        &mut self.components
370    }
371
372    pub(crate) fn refresh_list(&mut self) {
373        self.components.clear();
374        if let Ok(dir) = read_dir(Path::new("/sys/class/hwmon/")) {
375            for entry in dir.flatten() {
376                let Ok(file_type) = entry.file_type() else {
377                    continue;
378                };
379                let entry = entry.path();
380                if !file_type.is_file()
381                    && entry
382                        .file_name()
383                        .and_then(|x| x.to_str())
384                        .unwrap_or("")
385                        .starts_with("hwmon")
386                {
387                    ComponentInner::from_hwmon(&mut self.components, &entry);
388                }
389            }
390        }
391    }
392}