1use std::collections::HashMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use sysinfo::System;
9
10use crate::AVAILABLE_GOVERNORS_SORTED;
11use crate::CONFIG;
12use crate::POWER_SUPPLY_DIR;
13
14#[derive(Debug, Clone)]
15pub struct CoreInfo {
16 pub id: usize,
17 pub usage: f32,
18 pub temperature: f32,
19 pub frequency: f32,
20}
21
22#[derive(Debug, Clone)]
23pub struct BatteryInfo {
24 pub is_charging: Option<bool>,
25 pub is_ac_plugged: Option<bool>,
26 pub charging_start_threshold: Option<i32>,
27 pub charging_stop_threshold: Option<i32>,
28 pub battery_level: Option<u8>,
29 pub power_consumption: Option<f32>,
30}
31
32#[derive(Debug, Clone)]
33pub struct SystemReport {
34 pub distro_name: String,
35 pub distro_ver: String,
36 pub arch: String,
37 pub processor_model: String,
38 pub total_core: Option<usize>,
39 pub kernel_version: String,
40 pub current_gov: Option<String>,
41 pub current_epp: Option<String>,
42 pub current_epb: Option<String>,
43 pub cpu_driver: Option<String>,
44 pub cpu_fan_speed: Option<i32>,
45 pub cpu_usage: f32,
46 pub cpu_max_freq: Option<f32>,
47 pub cpu_min_freq: Option<f32>,
48 pub load: f32,
49 pub avg_load: Option<(f32, f32, f32)>,
50 pub cores_info: Vec<CoreInfo>,
51 pub battery_info: BatteryInfo,
52 pub is_turbo_on: (Option<bool>, Option<bool>),
53}
54
55struct TempSensorCache {
59 sensor_paths: HashMap<usize, PathBuf>,
60 package_temp_path: Option<PathBuf>,
61 fan_speed_path: Option<PathBuf>,
62 last_scan: Instant,
63}
64
65impl TempSensorCache {
66 fn new() -> Self {
67 let mut cache = Self {
68 sensor_paths: HashMap::new(),
69 package_temp_path: None,
70 fan_speed_path: None,
71 last_scan: Instant::now(),
72 };
73 cache.scan_sensors();
74 cache
75 }
76
77 fn scan_sensors(&mut self) {
78 let sensor_priority = ["coretemp", "k10temp", "zenpower", "acpitz"];
79 let hwmon_path = "/sys/class/hwmon";
80
81 if let Ok(entries) = fs::read_dir(hwmon_path) {
82 for entry in entries.flatten() {
83 let path = entry.path();
84 let name_file = path.join("name");
85
86 if let Ok(sensor_name) = fs::read_to_string(&name_file) {
87 let sensor_name = sensor_name.trim();
88
89 if sensor_priority.contains(&sensor_name) {
90 let pkg_temp = path.join("temp1_input");
91 if pkg_temp.exists() {
92 self.package_temp_path = Some(pkg_temp);
93 }
94
95 for temp_id in 2..20 {
96 let temp_file = path.join(format!("temp{}_input", temp_id));
97 if temp_file.exists() {
98 let core_id = temp_id - 2;
99 self.sensor_paths.insert(core_id, temp_file);
100 }
101 }
102 }
103
104 if self.fan_speed_path.is_none() {
105 let fan_input = path.join("fan1_input");
106 if fan_input.exists() {
107 self.fan_speed_path = Some(fan_input);
108 }
109 }
110 }
111 }
112 }
113
114 self.last_scan = Instant::now();
115 }
116
117 fn read_core_temp(&self, core_id: usize) -> f32 {
118 if let Some(path) = self.sensor_paths.get(&core_id) {
119 if let Ok(temp_str) = fs::read_to_string(path) {
120 if let Ok(temp) = temp_str.trim().parse::<f32>() {
121 return temp / 1000.0;
122 }
123 }
124 }
125
126 if let Some(ref path) = self.package_temp_path {
127 if let Ok(temp_str) = fs::read_to_string(path) {
128 if let Ok(temp) = temp_str.trim().parse::<f32>() {
129 return temp / 1000.0;
130 }
131 }
132 }
133
134 0.0
135 }
136
137 fn read_fan_speed(&self) -> Option<i32> {
138 if let Some(ref path) = self.fan_speed_path {
139 if let Ok(fan_str) = fs::read_to_string(path) {
140 if let Ok(rpm) = fan_str.trim().parse::<i32>() {
141 if rpm > 0 {
142 return Some(rpm);
143 }
144 }
145 }
146 }
147 None
148 }
149}
150
151lazy_static::lazy_static! {
152 static ref TEMP_CACHE: Arc<Mutex<TempSensorCache>> = Arc::new(Mutex::new(TempSensorCache::new()));
153}
154
155struct StaticInfoCache {
159 processor_model: String,
160 cpu_driver: Option<String>,
161 cpu_min_freq: Option<f32>,
162 cpu_max_freq: Option<f32>,
163}
164
165impl StaticInfoCache {
166 fn new() -> Self {
167 Self {
168 processor_model: Self::read_processor_model(),
169 cpu_driver: Self::read_cpu_driver(),
170 cpu_min_freq: Self::read_cpu_min_freq(),
171 cpu_max_freq: Self::read_cpu_max_freq(),
172 }
173 }
174
175 fn read_processor_model() -> String {
176 fs::read_to_string("/proc/cpuinfo")
177 .ok()
178 .and_then(|s| {
179 s.lines()
180 .find(|l| l.contains("model name"))
181 .and_then(|l| l.split(':').nth(1))
182 .map(|s| s.trim().to_string())
183 })
184 .unwrap_or_default()
185 }
186
187 fn read_cpu_driver() -> Option<String> {
188 fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver")
189 .ok()
190 .map(|s| s.trim().to_string())
191 }
192
193 fn read_cpu_min_freq() -> Option<f32> {
194 fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq")
195 .ok()
196 .and_then(|s| s.trim().parse::<f32>().ok())
197 .map(|khz| khz / 1000.0)
198 }
199
200 fn read_cpu_max_freq() -> Option<f32> {
201 fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq")
202 .ok()
203 .and_then(|s| s.trim().parse::<f32>().ok())
204 .map(|khz| khz / 1000.0)
205 }
206}
207
208lazy_static::lazy_static! {
209 static ref STATIC_INFO: StaticInfoCache = StaticInfoCache::new();
210}
211
212struct BatteryPathCache {
216 battery_path: Option<PathBuf>,
217 mains_path: Option<PathBuf>,
218 cached_at: Instant,
219}
220
221impl BatteryPathCache {
222 fn new() -> Self {
223 let (battery_path, mains_path) = Self::scan_power_supply();
224 Self {
225 battery_path,
226 mains_path,
227 cached_at: Instant::now(),
228 }
229 }
230
231 fn scan_power_supply() -> (Option<PathBuf>, Option<PathBuf>) {
232 let mut battery = None;
233 let mut mains = None;
234
235 if CONFIG.has_option("battery", "battery_device") {
236 let battery_device = CONFIG.get("battery", "battery_device", "");
237 if !battery_device.is_empty() {
238 let custom = Path::new(POWER_SUPPLY_DIR).join(&battery_device);
239 let type_path = custom.join("type");
240 if type_path.is_file() {
241 if let Ok(content) = fs::read_to_string(type_path) {
242 if content.trim().to_lowercase() == "battery" {
243 battery = Some(custom);
244 }
245 }
246 }
247 }
248 }
249
250 if let Ok(entries) = fs::read_dir(POWER_SUPPLY_DIR) {
251 for entry in entries.flatten() {
252 let path = entry.path();
253 let type_path = path.join("type");
254
255 if let Ok(content) = fs::read_to_string(&type_path) {
256 match content.trim() {
257 "Battery" if battery.is_none() => battery = Some(path),
258 "Mains" if mains.is_none() => mains = Some(path),
259 _ => {}
260 }
261 }
262 }
263 }
264
265 (battery, mains)
266 }
267
268 fn maybe_rescan(&mut self) {
269 if self.cached_at.elapsed() > Duration::from_secs(60) {
270 let (battery, mains) = Self::scan_power_supply();
271 self.battery_path = battery;
272 self.mains_path = mains;
273 self.cached_at = Instant::now();
274 }
275 }
276}
277
278lazy_static::lazy_static! {
279 static ref BATTERY_PATH_CACHE: Arc<Mutex<BatteryPathCache>> =
280 Arc::new(Mutex::new(BatteryPathCache::new()));
281}
282
283pub struct SystemInfo {
287 pub distro_name: String,
288 pub distro_version: String,
289 pub architecture: String,
290 pub processor_model: String,
291 pub total_cores: Option<usize>,
292 pub cpu_driver: Option<String>,
293 pub kernel_version: String,
294}
295
296impl Default for SystemInfo {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302impl SystemInfo {
303 pub fn new() -> Self {
304 let distro_name = Self::read_os_release_name().unwrap_or_else(|| "UNKNOWN".into());
305 let distro_version = Self::read_os_release_version().unwrap_or_else(|| "UNKNOWN".into());
306 let architecture = std::env::consts::ARCH.to_string();
307 let total_cores = Some(num_cpus::get());
308 let kernel_version = Self::uname_release().unwrap_or_default();
309
310 Self {
311 distro_name,
312 distro_version,
313 architecture,
314 processor_model: STATIC_INFO.processor_model.clone(),
315 total_cores,
316 cpu_driver: STATIC_INFO.cpu_driver.clone(),
317 kernel_version,
318 }
319 }
320
321 fn read_os_release_name() -> Option<String> {
322 if let Ok(content) = fs::read_to_string("/etc/os-release") {
323 for line in content.lines() {
324 if line.starts_with("PRETTY_NAME=") {
325 return Some(
326 line.split_once('=')
327 .map(|x| x.1)
328 .unwrap_or("")
329 .trim_matches('"')
330 .to_string(),
331 );
332 }
333 }
334 }
335 None
336 }
337
338 fn read_os_release_version() -> Option<String> {
339 if let Ok(content) = fs::read_to_string("/etc/os-release") {
340 for line in content.lines() {
341 if line.starts_with("VERSION=") {
342 return Some(
343 line.split_once('=')
344 .map(|x| x.1)
345 .unwrap_or("")
346 .trim_matches('"')
347 .to_string(),
348 );
349 }
350 }
351 }
352 None
353 }
354
355 fn uname_release() -> Option<String> {
356 std::process::Command::new("uname")
357 .arg("-r")
358 .output()
359 .ok()
360 .and_then(|o| String::from_utf8(o.stdout).ok())
361 .map(|s| s.trim().to_string())
362 }
363
364 pub fn cpu_min_freq() -> Option<f32> {
365 STATIC_INFO.cpu_min_freq
366 }
367
368 pub fn cpu_max_freq() -> Option<f32> {
369 STATIC_INFO.cpu_max_freq
370 }
371
372 pub fn get_cpu_info(sys: &System) -> Vec<CoreInfo> {
373 let cpus = sys.cpus();
374 let mut cores = Vec::with_capacity(cpus.len());
375
376 let temp_cache = TEMP_CACHE.lock().unwrap();
377
378 for (i, cpu) in cpus.iter().enumerate() {
379 cores.push(CoreInfo {
380 id: i,
381 usage: cpu.cpu_usage(),
382 frequency: cpu.frequency() as f32,
383 temperature: temp_cache.read_core_temp(i),
384 });
385 }
386
387 cores
388 }
389
390 pub fn cpu_fan_speed() -> Option<i32> {
391 TEMP_CACHE.lock().unwrap().read_fan_speed()
392 }
393
394 pub fn current_gov() -> Option<String> {
395 fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
396 .ok()
397 .map(|s| s.trim().to_string())
398 }
399
400 pub fn current_epp(is_ac_plugged: bool) -> Option<String> {
401 let epp_path =
402 Path::new("/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference");
403 if !epp_path.exists() {
404 return None;
405 }
406
407 let section = if is_ac_plugged { "charger" } else { "battery" };
408 Some(CONFIG.get(section, "energy_performance_preference", "balance_power"))
409 }
410
411 pub fn current_epb(is_ac_plugged: bool) -> Option<String> {
412 let epb_path = Path::new("/sys/devices/system/cpu/intel_pstate");
413 if !epb_path.exists() {
414 return None;
415 }
416 let section = if is_ac_plugged { "charger" } else { "battery" };
417 Some(CONFIG.get(section, "energy_perf_bias", "balance_power"))
418 }
419
420 pub fn cpu_usage(sys: &System) -> f32 {
421 let cpus = sys.cpus();
422 if cpus.is_empty() {
423 return 0.0;
424 }
425 let sum: f32 = cpus.iter().map(|c| c.cpu_usage()).sum();
426 sum / (cpus.len() as f32)
427 }
428
429 pub fn system_load() -> f32 {
430 if let Ok(s) = fs::read_to_string("/proc/loadavg") {
431 if let Some(first) = s.split_whitespace().next() {
432 return first.parse::<f32>().unwrap_or(0.0);
433 }
434 }
435 0.0
436 }
437
438 pub fn avg_load() -> Option<(f32, f32, f32)> {
439 if let Ok(s) = fs::read_to_string("/proc/loadavg") {
440 let mut parts = s.split_whitespace();
441 let a = parts.next().and_then(|p| p.parse::<f32>().ok());
442 let b = parts.next().and_then(|p| p.parse::<f32>().ok());
443 let c = parts.next().and_then(|p| p.parse::<f32>().ok());
444 if let (Some(a), Some(b), Some(c)) = (a, b, c) {
445 return Some((a, b, c));
446 }
447 }
448 None
449 }
450
451 pub fn avg_temp(sys: &System) -> i32 {
452 let temps: Vec<f32> = Self::get_cpu_info(sys)
453 .iter()
454 .map(|c| c.temperature)
455 .filter(|&t| t > 0.0)
456 .collect();
457
458 if temps.is_empty() {
459 0
460 } else {
461 (temps.iter().sum::<f32>() / temps.len() as f32) as i32
462 }
463 }
464
465 pub fn turbo_on() -> (Option<bool>, Option<bool>) {
466 let intel_pstate = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo");
467 let cpu_freq = Path::new("/sys/devices/system/cpu/cpufreq/boost");
468 let amd_pstate = Path::new("/sys/devices/system/cpu/amd_pstate/status");
469
470 if intel_pstate.exists() {
471 if let Ok(v) = fs::read_to_string(intel_pstate) {
472 if let Ok(n) = v.trim().parse::<i32>() {
473 return (Some(n == 0), Some(false));
474 }
475 }
476 return (None, None);
477 }
478
479 if cpu_freq.exists() {
480 if let Ok(v) = fs::read_to_string(cpu_freq) {
481 if let Ok(n) = v.trim().parse::<i32>() {
482 return (Some(n != 0), Some(false));
483 }
484 }
485 return (None, None);
486 }
487
488 if amd_pstate.exists() {
489 if let Ok(s) = fs::read_to_string(amd_pstate) {
490 if s.trim() == "active" {
491 return (None, Some(true));
492 }
493 return (None, Some(false));
494 }
495 return (None, None);
496 }
497
498 (None, None)
499 }
500
501 pub fn get_battery_path() -> Option<PathBuf> {
502 let mut cache = BATTERY_PATH_CACHE.lock().unwrap();
503 cache.maybe_rescan();
504 cache.battery_path.clone()
505 }
506
507 pub fn battery_info() -> BatteryInfo {
508 let mut cache = BATTERY_PATH_CACHE.lock().unwrap();
509 cache.maybe_rescan();
510
511 let mut is_ac_plugged = Some(true);
512
513 if let Some(ref mains_path) = cache.mains_path {
514 if let Ok(online) = fs::read_to_string(mains_path.join("online")) {
515 is_ac_plugged = Some(online.trim() == "1");
516 }
517 }
518
519 let battery_path = match &cache.battery_path {
520 Some(p) => p,
521 None => {
522 return BatteryInfo {
523 is_charging: None,
524 is_ac_plugged: Some(true),
525 charging_start_threshold: None,
526 charging_stop_threshold: None,
527 battery_level: None,
528 power_consumption: None,
529 };
530 }
531 };
532
533 let status = fs::read_to_string(battery_path.join("status")).ok();
534 let capacity = fs::read_to_string(battery_path.join("capacity")).ok();
535 let energy_rate = fs::read_to_string(battery_path.join("power_now"))
536 .or_else(|_| fs::read_to_string(battery_path.join("current_now")))
537 .ok();
538 let charge_start = fs::read_to_string(battery_path.join("charge_start_threshold"))
539 .or_else(|_| fs::read_to_string(battery_path.join("charge_control_start_threshold")))
540 .ok();
541 let charge_stop = fs::read_to_string(battery_path.join("charge_stop_threshold"))
542 .or_else(|_| fs::read_to_string(battery_path.join("charge_control_end_threshold")))
543 .ok();
544
545 let is_charging = status
546 .as_ref()
547 .map(|s| s.trim().to_lowercase() == "charging");
548 let battery_level = capacity.and_then(|c| c.trim().parse::<u8>().ok());
549 let power_consumption = energy_rate
550 .and_then(|e| e.trim().parse::<f32>().ok())
551 .map(|v| v / 1_000_000.0);
552 let charging_start_threshold = charge_start.and_then(|s| s.trim().parse::<i32>().ok());
553 let charging_stop_threshold = charge_stop.and_then(|s| s.trim().parse::<i32>().ok());
554
555 BatteryInfo {
556 is_charging,
557 is_ac_plugged,
558 charging_start_threshold,
559 charging_stop_threshold,
560 battery_level,
561 power_consumption,
562 }
563 }
564
565 pub fn turbo_on_suggestion(sys: &System) -> bool {
566 let usage = Self::cpu_usage(sys);
567 if usage >= 20.0 {
568 return true;
569 }
570 if usage <= 25.0 && Self::avg_temp(sys) as f32 >= 70.0 {
571 return false;
572 }
573 false
574 }
575
576 pub fn governor_suggestion() -> Option<String> {
577 let batt = Self::battery_info();
578 if batt.is_ac_plugged.unwrap_or(true) {
579 AVAILABLE_GOVERNORS_SORTED.first().cloned()
580 } else {
581 AVAILABLE_GOVERNORS_SORTED.last().cloned()
582 }
583 }
584
585 pub fn generate_system_report(&self, sys: &System) -> SystemReport {
586 let battery = Self::battery_info();
587 let cores = Self::get_cpu_info(sys);
588
589 SystemReport {
590 distro_name: self.distro_name.clone(),
591 distro_ver: self.distro_version.clone(),
592 arch: self.architecture.clone(),
593 processor_model: self.processor_model.clone(),
594 total_core: self.total_cores,
595 kernel_version: self.kernel_version.clone(),
596 current_gov: Self::current_gov(),
597 current_epp: battery.is_ac_plugged.and_then(Self::current_epp),
598 current_epb: battery.is_ac_plugged.and_then(Self::current_epb),
599 cpu_driver: self.cpu_driver.clone(),
600 cpu_fan_speed: Self::cpu_fan_speed(),
601 cpu_usage: Self::cpu_usage(sys),
602 cpu_max_freq: Self::cpu_max_freq(),
603 cpu_min_freq: Self::cpu_min_freq(),
604 load: Self::system_load(),
605 avg_load: Self::avg_load(),
606 cores_info: cores,
607 battery_info: battery,
608 is_turbo_on: Self::turbo_on(),
609 }
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn smoke() {
619 let s = SystemInfo::new();
620 let mut sys = System::new_all();
621 sys.refresh_cpu_all();
622 std::thread::sleep(std::time::Duration::from_millis(200));
623 sys.refresh_cpu_all();
624 let _ = s.generate_system_report(&sys);
625 }
626
627 #[test]
628 fn test_temp_cache() {
629 let cache = TEMP_CACHE.lock().unwrap();
630 let temp = cache.read_core_temp(0);
631 assert!(temp >= 0.0);
632 }
633
634 #[test]
635 fn test_battery_cache() {
636 let cache = BATTERY_PATH_CACHE.lock().unwrap();
637 let _ = cache.battery_path.is_some();
638 }
639}