mirror of
https://github.com/RGBCube/superfreq
synced 2025-08-02 11:57:46 +00:00
Compare commits
5 commits
d5dbb36de4
...
dfa788009c
Author | SHA1 | Date | |
---|---|---|---|
dfa788009c | |||
fb5a891d42 | |||
542c41ccbe | |||
45a9fd4749 | |||
ec34526012 |
9 changed files with 288 additions and 302 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -261,6 +261,12 @@ version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
|
@ -271,6 +277,17 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi 0.5.1",
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
|
@ -364,7 +381,7 @@ version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi 0.3.9",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -697,3 +714,6 @@ name = "yansi"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
dependencies = [
|
||||||
|
"is-terminal",
|
||||||
|
]
|
||||||
|
|
|
@ -19,5 +19,5 @@ thiserror = "2.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
jiff = "0.2.13"
|
jiff = "0.2.13"
|
||||||
clap-verbosity-flag = "3.0.2"
|
clap-verbosity-flag = "3.0.2"
|
||||||
yansi = "1.0.1"
|
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
|
||||||
derive_more = { version = "2.0.1", features = ["full"] }
|
derive_more = { version = "2.0.1", features = ["full"] }
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
pub struct SystemInfo {
|
pub struct SystemInfo {
|
||||||
// Overall system details
|
// Overall system details
|
||||||
pub cpu_model: String,
|
pub cpu_model: String,
|
||||||
pub architecture: String,
|
|
||||||
pub linux_distribution: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CpuCoreInfo {
|
pub struct CpuCoreInfo {
|
||||||
|
|
107
src/cpu.rs
107
src/cpu.rs
|
@ -5,10 +5,37 @@ use std::{fmt, string::ToString};
|
||||||
|
|
||||||
use crate::fs;
|
use crate::fs;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Cpu {
|
pub struct Cpu {
|
||||||
pub number: u32,
|
pub number: u32,
|
||||||
|
|
||||||
pub has_cpufreq: bool,
|
pub has_cpufreq: bool,
|
||||||
|
|
||||||
|
pub time_user: u64,
|
||||||
|
pub time_nice: u64,
|
||||||
|
pub time_system: u64,
|
||||||
|
pub time_idle: u64,
|
||||||
|
pub time_iowait: u64,
|
||||||
|
pub time_irq: u64,
|
||||||
|
pub time_softirq: u64,
|
||||||
|
pub time_steal: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cpu {
|
||||||
|
pub fn time_total(&self) -> u64 {
|
||||||
|
self.time_user
|
||||||
|
+ self.time_nice
|
||||||
|
+ self.time_system
|
||||||
|
+ self.time_idle
|
||||||
|
+ self.time_iowait
|
||||||
|
+ self.time_irq
|
||||||
|
+ self.time_softirq
|
||||||
|
+ self.time_steal
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_idle(&self) -> u64 {
|
||||||
|
self.time_idle + self.time_iowait
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Cpu {
|
impl fmt::Display for Cpu {
|
||||||
|
@ -24,6 +51,15 @@ impl Cpu {
|
||||||
let mut cpu = Self {
|
let mut cpu = Self {
|
||||||
number,
|
number,
|
||||||
has_cpufreq: false,
|
has_cpufreq: false,
|
||||||
|
|
||||||
|
time_user: 0,
|
||||||
|
time_nice: 0,
|
||||||
|
time_system: 0,
|
||||||
|
time_idle: 0,
|
||||||
|
time_iowait: 0,
|
||||||
|
time_irq: 0,
|
||||||
|
time_softirq: 0,
|
||||||
|
time_steal: 0,
|
||||||
};
|
};
|
||||||
cpu.rescan()?;
|
cpu.rescan()?;
|
||||||
|
|
||||||
|
@ -76,9 +112,70 @@ impl Cpu {
|
||||||
bail!("{self} does not exist");
|
bail!("{self} does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
|
self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
|
||||||
|
|
||||||
self.has_cpufreq = has_cpufreq;
|
self.rescan_times()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescan_times(&mut self) -> anyhow::Result<()> {
|
||||||
|
// TODO: Don't read this per CPU. Share the read or
|
||||||
|
// find something in /sys/.../cpu{N} that does it.
|
||||||
|
let content = fs::read("/proc/stat")
|
||||||
|
.context("/proc/stat does not exist")?
|
||||||
|
.context("failed to read CPU stat")?;
|
||||||
|
|
||||||
|
let cpu_name = format!("cpu{number}", number = self.number);
|
||||||
|
|
||||||
|
let mut stats = content
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| {
|
||||||
|
line.starts_with(&cpu_name)
|
||||||
|
.then(|| line.split_whitespace().skip(1))
|
||||||
|
})
|
||||||
|
.with_context(|| format!("failed to find {self} in CPU stats"))?;
|
||||||
|
|
||||||
|
self.time_user = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} user time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} user time"))?;
|
||||||
|
self.time_nice = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} nice time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} nice time"))?;
|
||||||
|
self.time_system = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} system time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} system time"))?;
|
||||||
|
self.time_idle = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} idle time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} idle time"))?;
|
||||||
|
self.time_iowait = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} iowait time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} iowait time"))?;
|
||||||
|
self.time_irq = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} irq time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} irq time"))?;
|
||||||
|
self.time_softirq = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} softirq time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} softirq time"))?;
|
||||||
|
self.time_steal = stats
|
||||||
|
.next()
|
||||||
|
.with_context(|| format!("failed to find {self} steal time"))?
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse {self} steal time"))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -232,7 +329,7 @@ impl Cpu {
|
||||||
fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
let Self { number, .. } = self;
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
let Ok(minimum_frequency_khz) = fs::read_u64(format!(
|
let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!(
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||||
)) else {
|
)) else {
|
||||||
// Just let it pass if we can't find anything.
|
// Just let it pass if we can't find anything.
|
||||||
|
@ -270,7 +367,7 @@ impl Cpu {
|
||||||
fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> {
|
||||||
let Self { number, .. } = self;
|
let Self { number, .. } = self;
|
||||||
|
|
||||||
let Ok(maximum_frequency_khz) = fs::read_u64(format!(
|
let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!(
|
||||||
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
||||||
)) else {
|
)) else {
|
||||||
// Just let it pass if we can't find anything.
|
// Just let it pass if we can't find anything.
|
||||||
|
|
19
src/fs.rs
19
src/fs.rs
|
@ -29,18 +29,19 @@ pub fn read(path: impl AsRef<Path>) -> Option<anyhow::Result<String>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_u64(path: impl AsRef<Path>) -> anyhow::Result<u64> {
|
pub fn read_u64(path: impl AsRef<Path>) -> Option<anyhow::Result<u64>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
let content = fs::read_to_string(path)
|
match read(path)? {
|
||||||
.with_context(|| format!("failed to read '{path}'", path = path.display()))?;
|
Ok(content) => Some(content.trim().parse().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse contents of '{path}' as a unsigned number",
|
||||||
|
path = path.display(),
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
|
||||||
Ok(content.trim().parse().with_context(|| {
|
Err(error) => Some(Err(error)),
|
||||||
format!(
|
}
|
||||||
"failed to parse contents of '{path}' as a unsigned number",
|
|
||||||
path = path.display(),
|
|
||||||
)
|
|
||||||
})?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
pub fn write(path: impl AsRef<Path>, value: &str) -> anyhow::Result<()> {
|
||||||
|
|
|
@ -36,7 +36,7 @@ enum Command {
|
||||||
/// Start the daemon.
|
/// Start the daemon.
|
||||||
Start {
|
Start {
|
||||||
/// The daemon config path.
|
/// The daemon config path.
|
||||||
#[arg(long, env = "SUPERFREQ_CONFIG")]
|
#[arg(long, env = "WATT_CONFIG")]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -50,6 +50,8 @@ enum Command {
|
||||||
fn real_main() -> anyhow::Result<()> {
|
fn real_main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
yansi::whenever(yansi::Condition::TTY_AND_COLOR);
|
||||||
|
|
||||||
env_logger::Builder::new()
|
env_logger::Builder::new()
|
||||||
.filter_level(cli.verbosity.log_level_filter())
|
.filter_level(cli.verbosity.log_level_filter())
|
||||||
.format_timestamp(None)
|
.format_timestamp(None)
|
||||||
|
|
280
src/monitor.rs
280
src/monitor.rs
|
@ -10,142 +10,10 @@ use std::{
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read a sysfs file to a string, trimming whitespace
|
|
||||||
fn read_sysfs_file_trimmed(path: impl AsRef<Path>) -> anyhow::Result<String> {
|
|
||||||
fs::read_to_string(path.as_ref())
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.map_err(|e| {
|
|
||||||
SysMonitorError::ReadError(format!("Path: {:?}, Error: {}", path.as_ref().display(), e))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read a sysfs file and parse it to a specific type
|
|
||||||
fn read_sysfs_value<T: FromStr>(path: impl AsRef<Path>) -> anyhow::Result<T> {
|
|
||||||
let content = read_sysfs_file_trimmed(path.as_ref())?;
|
|
||||||
content.parse::<T>().map_err(|_| {
|
|
||||||
SysMonitorError::ParseError(format!(
|
|
||||||
"Could not parse '{}' from {:?}",
|
|
||||||
content,
|
|
||||||
path.as_ref().display()
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_system_info() -> SystemInfo {
|
pub fn get_system_info() -> SystemInfo {
|
||||||
let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string());
|
let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string());
|
||||||
let linux_distribution = get_linux_distribution().unwrap_or_else(|_| "Unknown".to_string());
|
|
||||||
let architecture = std::env::consts::ARCH.to_string();
|
|
||||||
|
|
||||||
SystemInfo {
|
SystemInfo { cpu_model }
|
||||||
cpu_model,
|
|
||||||
architecture,
|
|
||||||
linux_distribution,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct CpuTimes {
|
|
||||||
user: u64,
|
|
||||||
nice: u64,
|
|
||||||
system: u64,
|
|
||||||
idle: u64,
|
|
||||||
iowait: u64,
|
|
||||||
irq: u64,
|
|
||||||
softirq: u64,
|
|
||||||
steal: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CpuTimes {
|
|
||||||
const fn total_time(&self) -> u64 {
|
|
||||||
self.user
|
|
||||||
+ self.nice
|
|
||||||
+ self.system
|
|
||||||
+ self.idle
|
|
||||||
+ self.iowait
|
|
||||||
+ self.irq
|
|
||||||
+ self.softirq
|
|
||||||
+ self.steal
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn idle_time(&self) -> u64 {
|
|
||||||
self.idle + self.iowait
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_all_cpu_times() -> anyhow::Result<HashMap<u32, CpuTimes>> {
|
|
||||||
let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?;
|
|
||||||
let mut cpu_times_map = HashMap::new();
|
|
||||||
|
|
||||||
for line in content.lines() {
|
|
||||||
if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) {
|
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
||||||
if parts.len() < 11 {
|
|
||||||
return Err(SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Line too short: {line}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let core_id_str = &parts[0][3..];
|
|
||||||
let core_id = core_id_str.parse::<u32>().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse core_id: {core_id_str}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let times = CpuTimes {
|
|
||||||
user: parts[1].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse user time: {}",
|
|
||||||
parts[1]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
nice: parts[2].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse nice time: {}",
|
|
||||||
parts[2]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
system: parts[3].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse system time: {}",
|
|
||||||
parts[3]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
idle: parts[4].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse idle time: {}",
|
|
||||||
parts[4]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
iowait: parts[5].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse iowait time: {}",
|
|
||||||
parts[5]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
irq: parts[6].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse irq time: {}",
|
|
||||||
parts[6]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
softirq: parts[7].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse softirq time: {}",
|
|
||||||
parts[7]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
steal: parts[8].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ProcStatParseError(format!(
|
|
||||||
"Failed to parse steal time: {}",
|
|
||||||
parts[8]
|
|
||||||
))
|
|
||||||
})?,
|
|
||||||
};
|
|
||||||
cpu_times_map.insert(core_id, times);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(cpu_times_map)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cpu_core_info(
|
pub fn get_cpu_core_info(
|
||||||
|
@ -599,110 +467,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result<Vec<BatteryInfo>>
|
||||||
Ok(batteries)
|
Ok(batteries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a battery is likely a peripheral (mouse, keyboard, etc) not a laptop battery
|
|
||||||
fn is_peripheral_battery(ps_path: &Path, name: &str) -> bool {
|
|
||||||
// Convert name to lowercase once for case-insensitive matching
|
|
||||||
let name_lower = name.to_lowercase();
|
|
||||||
|
|
||||||
// Common peripheral battery names
|
|
||||||
if name_lower.contains("mouse")
|
|
||||||
|| name_lower.contains("keyboard")
|
|
||||||
|| name_lower.contains("trackpad")
|
|
||||||
|| name_lower.contains("gamepad")
|
|
||||||
|| name_lower.contains("controller")
|
|
||||||
|| name_lower.contains("headset")
|
|
||||||
|| name_lower.contains("headphone")
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small capacity batteries are likely not laptop batteries
|
|
||||||
if let Ok(energy_full) = read_sysfs_value::<i32>(ps_path.join("energy_full")) {
|
|
||||||
// Most laptop batteries are at least 20,000,000 µWh (20 Wh)
|
|
||||||
// Peripheral batteries are typically much smaller
|
|
||||||
if energy_full < 10_000_000 {
|
|
||||||
// 10 Wh in µWh
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for model name that indicates a peripheral
|
|
||||||
if let Ok(model) = read_sysfs_file_trimmed(ps_path.join("model_name")) {
|
|
||||||
if model.contains("bluetooth") || model.contains("wireless") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine if this is likely a desktop system rather than a laptop
|
|
||||||
fn is_likely_desktop_system() -> bool {
|
|
||||||
// Check for DMI system type information
|
|
||||||
if let Ok(chassis_type) = fs::read_to_string("/sys/class/dmi/id/chassis_type") {
|
|
||||||
let chassis_type = chassis_type.trim();
|
|
||||||
|
|
||||||
// Chassis types:
|
|
||||||
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower
|
|
||||||
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One
|
|
||||||
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis
|
|
||||||
match chassis_type {
|
|
||||||
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return true, // desktop form factors
|
|
||||||
"9" | "10" | "14" => return false, // laptop form factors
|
|
||||||
_ => {} // Unknown, continue with other checks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check CPU power policies, desktops often don't have these
|
|
||||||
let power_saving_exists = Path::new("/sys/module/intel_pstate/parameters/no_hwp").exists()
|
|
||||||
|| Path::new("/sys/devices/system/cpu/cpufreq/conservative").exists();
|
|
||||||
|
|
||||||
if !power_saving_exists {
|
|
||||||
return true; // likely a desktop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check battery-specific ACPI paths that laptops typically have
|
|
||||||
let laptop_acpi_paths = [
|
|
||||||
"/sys/class/power_supply/BAT0",
|
|
||||||
"/sys/class/power_supply/BAT1",
|
|
||||||
"/proc/acpi/battery",
|
|
||||||
];
|
|
||||||
|
|
||||||
for path in &laptop_acpi_paths {
|
|
||||||
if Path::new(path).exists() {
|
|
||||||
return false; // Likely a laptop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to assuming desktop if we can't determine
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_system_load() -> anyhow::Result<SystemLoad> {
|
|
||||||
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
|
|
||||||
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
|
||||||
if parts.len() < 3 {
|
|
||||||
return Err(SysMonitorError::ParseError(
|
|
||||||
"Could not parse /proc/loadavg: expected at least 3 parts".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let load_avg_1min = parts[0].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0]))
|
|
||||||
})?;
|
|
||||||
let load_avg_5min = parts[1].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1]))
|
|
||||||
})?;
|
|
||||||
let load_avg_15min = parts[2].parse().map_err(|_| {
|
|
||||||
SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2]))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(SystemLoad {
|
|
||||||
load_avg_1min,
|
|
||||||
load_avg_5min,
|
|
||||||
load_avg_15min,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> {
|
pub fn collect_system_report(config: &AppConfig) -> anyhow::Result<SystemReport> {
|
||||||
let system_info = get_system_info();
|
let system_info = get_system_info();
|
||||||
let cpu_cores = get_all_cpu_core_info()?;
|
let cpu_cores = get_all_cpu_core_info()?;
|
||||||
|
@ -738,45 +502,3 @@ pub fn get_cpu_model() -> anyhow::Result<String> {
|
||||||
"Could not find CPU model name in /proc/cpuinfo.".to_string(),
|
"Could not find CPU model name in /proc/cpuinfo.".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_linux_distribution() -> anyhow::Result<String> {
|
|
||||||
let os_release_path = Path::new("/etc/os-release");
|
|
||||||
let content = fs::read_to_string(os_release_path).map_err(|_| {
|
|
||||||
SysMonitorError::ReadError(format!(
|
|
||||||
"Cannot read contents of {}.",
|
|
||||||
os_release_path.display()
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for line in content.lines() {
|
|
||||||
if line.starts_with("PRETTY_NAME=") {
|
|
||||||
if let Some(val) = line.split('=').nth(1) {
|
|
||||||
let linux_distribution = val.trim_matches('"').to_string();
|
|
||||||
return Ok(linux_distribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lsb_release_path = Path::new("/etc/lsb-release");
|
|
||||||
let content = fs::read_to_string(lsb_release_path).map_err(|_| {
|
|
||||||
SysMonitorError::ReadError(format!(
|
|
||||||
"Cannot read contents of {}.",
|
|
||||||
lsb_release_path.display()
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for line in content.lines() {
|
|
||||||
if line.starts_with("DISTRIB_DESCRIPTION=") {
|
|
||||||
if let Some(val) = line.split('=').nth(1) {
|
|
||||||
let linux_distribution = val.trim_matches('"').to_string();
|
|
||||||
return Ok(linux_distribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(SysMonitorError::ParseError(format!(
|
|
||||||
"Could not find distribution name in {} or {}.",
|
|
||||||
os_release_path.display(),
|
|
||||||
lsb_release_path.display()
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,6 +48,9 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[
|
||||||
pub struct PowerSupply {
|
pub struct PowerSupply {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
pub is_from_peripheral: bool,
|
||||||
|
|
||||||
pub threshold_config: Option<PowerSupplyThresholdConfig>,
|
pub threshold_config: Option<PowerSupplyThresholdConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +77,9 @@ impl PowerSupply {
|
||||||
let mut power_supply = Self {
|
let mut power_supply = Self {
|
||||||
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
path: Path::new(POWER_SUPPLY_PATH).join(&name),
|
||||||
name,
|
name,
|
||||||
|
|
||||||
|
is_from_peripheral: false,
|
||||||
|
|
||||||
threshold_config: None,
|
threshold_config: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -94,6 +100,8 @@ impl PowerSupply {
|
||||||
|
|
||||||
path,
|
path,
|
||||||
|
|
||||||
|
is_from_peripheral: false,
|
||||||
|
|
||||||
threshold_config: None,
|
threshold_config: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -138,7 +146,7 @@ impl PowerSupply {
|
||||||
bail!("{self} does not exist");
|
bail!("{self} does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
let threshold_config = self
|
self.threshold_config = self
|
||||||
.get_type()
|
.get_type()
|
||||||
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?
|
.with_context(|| format!("failed to determine what type of power supply '{self}' is"))?
|
||||||
.eq("Battery")
|
.eq("Battery")
|
||||||
|
@ -155,7 +163,45 @@ impl PowerSupply {
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
self.threshold_config = threshold_config;
|
self.is_from_peripheral = 'is_from_peripheral: {
|
||||||
|
let name_lower = self.name.to_lowercase();
|
||||||
|
|
||||||
|
// Common peripheral battery names.
|
||||||
|
if name_lower.contains("mouse")
|
||||||
|
|| name_lower.contains("keyboard")
|
||||||
|
|| name_lower.contains("trackpad")
|
||||||
|
|| name_lower.contains("gamepad")
|
||||||
|
|| name_lower.contains("controller")
|
||||||
|
|| name_lower.contains("headset")
|
||||||
|
|| name_lower.contains("headphone")
|
||||||
|
{
|
||||||
|
break 'is_from_peripheral true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small capacity batteries are likely not laptop batteries.
|
||||||
|
if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) {
|
||||||
|
let energy_full = energy_full
|
||||||
|
.with_context(|| format!("failed to read the max charge '{self}' can hold"))?;
|
||||||
|
|
||||||
|
// Most laptop batteries are at least 20,000,000 µWh (20 Wh).
|
||||||
|
// Peripheral batteries are typically much smaller.
|
||||||
|
if energy_full < 10_000_000 {
|
||||||
|
// 10 Wh in µWh.
|
||||||
|
break 'is_from_peripheral true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for model name that indicates a peripheral
|
||||||
|
if let Some(model) = fs::read(self.path.join("model_name")) {
|
||||||
|
let model =
|
||||||
|
model.with_context(|| format!("failed to read the model name of '{self}'"))?;
|
||||||
|
|
||||||
|
if model.contains("bluetooth") || model.contains("wireless") {
|
||||||
|
break 'is_from_peripheral true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
104
src/system.rs
104
src/system.rs
|
@ -1,14 +1,114 @@
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
|
||||||
|
use crate::fs;
|
||||||
|
|
||||||
pub struct System {
|
pub struct System {
|
||||||
pub is_desktop: bool,
|
pub is_desktop: bool,
|
||||||
|
|
||||||
|
pub load_average_1min: f64,
|
||||||
|
pub load_average_5min: f64,
|
||||||
|
pub load_average_15min: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl System {
|
impl System {
|
||||||
pub fn new() -> anyhow::Result<Self> {
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
let mut system = Self { is_desktop: false };
|
let mut system = Self {
|
||||||
|
is_desktop: false,
|
||||||
|
|
||||||
|
load_average_1min: 0.0,
|
||||||
|
load_average_5min: 0.0,
|
||||||
|
load_average_15min: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
system.rescan()?;
|
system.rescan()?;
|
||||||
|
|
||||||
Ok(system)
|
Ok(system)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rescan(&mut self) -> anyhow::Result<()> {}
|
pub fn rescan(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.rescan_is_desktop()?;
|
||||||
|
self.rescan_load_average()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescan_is_desktop(&mut self) -> anyhow::Result<()> {
|
||||||
|
if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") {
|
||||||
|
let chassis_type = chassis_type.context("failed to read chassis type")?;
|
||||||
|
|
||||||
|
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower
|
||||||
|
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One
|
||||||
|
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis
|
||||||
|
match chassis_type.trim() {
|
||||||
|
// Desktop form factors.
|
||||||
|
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => {
|
||||||
|
self.is_desktop = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Laptop form factors.
|
||||||
|
"9" | "10" | "14" => {
|
||||||
|
self.is_desktop = false;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown, continue with other checks
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check CPU power policies, desktops often don't have these
|
||||||
|
let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|
||||||
|
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
|
||||||
|
|
||||||
|
if !power_saving_exists {
|
||||||
|
self.is_desktop = true;
|
||||||
|
return Ok(()); // Likely a desktop.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check battery-specific ACPI paths that laptops typically have
|
||||||
|
let laptop_acpi_paths = [
|
||||||
|
"/sys/class/power_supply/BAT0",
|
||||||
|
"/sys/class/power_supply/BAT1",
|
||||||
|
"/proc/acpi/battery",
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in laptop_acpi_paths {
|
||||||
|
if fs::exists(path) {
|
||||||
|
self.is_desktop = false; // Likely a laptop.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to assuming desktop if we can't determine.
|
||||||
|
self.is_desktop = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rescan_load_average(&mut self) -> anyhow::Result<()> {
|
||||||
|
let content = fs::read("/proc/loadavg")
|
||||||
|
.context("load average file doesn't exist, are you on linux?")?
|
||||||
|
.context("failed to read load average")?;
|
||||||
|
|
||||||
|
let mut parts = content.split_whitespace();
|
||||||
|
|
||||||
|
let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) =
|
||||||
|
(parts.next(), parts.next(), parts.next())
|
||||||
|
else {
|
||||||
|
bail!(
|
||||||
|
"failed to parse first 3 load average entries due to there not being enough, content: {content}"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.load_average_1min = load_average_1min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
self.load_average_5min = load_average_5min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
self.load_average_15min = load_average_15min
|
||||||
|
.parse()
|
||||||
|
.context("failed to parse load average")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue