1use 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 device_model: Option<String>,
18 name: String,
30 temperature: Option<f32>,
34 max: Option<f32>,
36 threshold_critical: Option<f32>,
49 sensor_type: Option<ThermalSensorType>,
63 label: String,
74 input_file: Option<PathBuf>,
91 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
128fn 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
137fn 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 let number = &reader[..n];
148 let number = std::str::from_utf8(number).ok()?;
149 let number = number.trim();
150 if cfg!(feature = "debug") {
152 assert!(!number.contains('\n') && !number.contains('\0'));
153 }
154 number.parse().ok()
155}
156
157#[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#[inline]
169fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
170 temp.map(|n| (n as f32) / 1000f32)
171}
172
173enum ThermalSensorType {
176 CPUEmbeddedDiode,
178 Transistor3904,
180 ThermalDiode,
182 Thermistor,
184 AMDAMDSI,
186 IntelPECI,
188 #[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
207fn 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 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 "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 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 .filter(|(_, c)| c.inner.input_file.is_some())
303 {
304 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 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 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 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 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 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}