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}