diff --git a/src/cpu.rs b/src/cpu.rs index ae45ffb..30165bd 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,46 +1,8 @@ use crate::core::{GovernorOverrideMode, TurboSetting}; +use crate::util::error::ControlError; use core::str; use std::{fs, io, path::Path, string::ToString}; -#[derive(Debug)] -pub enum ControlError { - Io(io::Error), - WriteError(String), - InvalidValueError(String), - NotSupported(String), - PermissionDenied(String), - InvalidProfile(String), -} - -impl From for ControlError { - fn from(err: io::Error) -> Self { - match err.kind() { - io::ErrorKind::PermissionDenied => Self::PermissionDenied(err.to_string()), - _ => Self::Io(err), - } - } -} - -impl std::fmt::Display for ControlError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Io(e) => write!(f, "I/O error: {e}"), - Self::WriteError(s) => write!(f, "Failed to write to sysfs path: {s}"), - Self::InvalidValueError(s) => write!(f, "Invalid value for setting: {s}"), - Self::NotSupported(s) => write!(f, "Control action not supported: {s}"), - Self::PermissionDenied(s) => { - write!(f, "Permission denied: {s}. Try running with sudo.") - } - Self::InvalidProfile(s) => { - write!( - f, - "Invalid platform control profile {s} supplied, please provide a valid one." - ) - } - } - } -} - impl std::error::Error for ControlError {} pub type Result = std::result::Result; @@ -58,16 +20,13 @@ fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<()> { }) } -fn for_each_cpu_core(mut action: F) -> Result<()> -where - F: FnMut(u32) -> Result<()>, -{ +pub fn get_logical_core_count() -> Result { // Using num_cpus::get() for a reliable count of logical cores accessible. // The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores, // but for applying settings, we might want to iterate over all reported by OS. // However, settings usually apply to cores with cpufreq. // Let's use a similar discovery to monitor's get_logical_core_count - let mut cores_to_act_on = Vec::new(); + let mut num_cores: u32 = 0; let path = Path::new("/sys/devices/system/cpu"); if !path.exists() { return Err(ControlError::NotSupported(format!( @@ -97,20 +56,25 @@ where continue; } - if let Ok(core_id) = name[3..].parse::() { - cores_to_act_on.push(core_id); + if name[3..].parse::().is_ok() { + num_cores += 1; } } - if cores_to_act_on.is_empty() { + if num_cores == 0 { // Fallback if sysfs iteration above fails to find any cpufreq cores - #[allow(clippy::cast_possible_truncation)] - let num_cores = num_cpus::get() as u32; - for core_id in 0..num_cores { - cores_to_act_on.push(core_id); - } + num_cores = num_cpus::get() as u32; } - for core_id in cores_to_act_on { + Ok(num_cores) +} + +fn for_each_cpu_core(mut action: F) -> Result<()> +where + F: FnMut(u32) -> Result<()>, +{ + let num_cores: u32 = get_logical_core_count()?; + + for core_id in 0u32..num_cores { action(core_id)?; } Ok(()) @@ -264,13 +228,9 @@ pub fn get_platform_profiles() -> Result> { ))); } - let buf = fs::read(path) + let content = fs::read_to_string(path) .map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?; - let content = str::from_utf8(&buf).map_err(|_| { - ControlError::NotSupported(format!("No platform profile choices found at {path}.")) - })?; - Ok(content .split_whitespace() .map(ToString::to_string) diff --git a/src/engine.rs b/src/engine.rs index a047196..c898256 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,7 @@ use crate::config::{AppConfig, ProfileConfig}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; -use crate::cpu::{self, ControlError}; +use crate::cpu::{self}; +use crate::util::error::ControlError; #[derive(Debug)] pub enum EngineError { @@ -67,7 +68,7 @@ pub fn determine_and_apply_settings( // Otherwise, check the ac_connected status from the (first) battery. // XXX: This relies on the setting ac_connected in BatteryInfo being set correctly. let on_ac_power = report.batteries.is_empty() - || report.batteries.first().is_some_and(|b| b.ac_connected); + || report.batteries.first().map_or(false, |b| b.ac_connected); if on_ac_power { println!("Engine: On AC power, selecting Charger profile."); diff --git a/src/main.rs b/src/main.rs index f114a3f..83fb35c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,11 @@ mod cpu; mod daemon; mod engine; mod monitor; +mod util; use crate::config::AppConfig; use crate::core::{GovernorOverrideMode, TurboSetting}; +use crate::util::error::ControlError; use clap::Parser; #[derive(Parser, Debug)] @@ -216,8 +218,8 @@ fn main() { // For example, check if e.downcast_ref::() matches PermissionDenied // and print a more specific message like "Try running with sudo." // We'll revisit this in the future once CPU logic is more stable. - if let Some(control_error) = e.downcast_ref::() { - if matches!(control_error, cpu::ControlError::PermissionDenied(_)) { + if let Some(control_error) = e.downcast_ref::() { + if matches!(control_error, ControlError::PermissionDenied(_)) { eprintln!( "Hint: This operation may require administrator privileges (e.g., run with sudo)." ); diff --git a/src/monitor.rs b/src/monitor.rs index a7918b3..a91bd19 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,5 +1,8 @@ use crate::config::AppConfig; use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; +use crate::cpu::{get_logical_core_count, get_platform_profiles}; +use crate::util::error::ControlError; +use crate::util::error::SysMonitorError; use std::{ collections::HashMap, fs, io, @@ -10,35 +13,6 @@ use std::{ time::SystemTime, }; -#[derive(Debug)] -pub enum SysMonitorError { - Io(io::Error), - ReadError(String), - ParseError(String), - ProcStatParseError(String), - NotAvailable(String), -} - -impl From for SysMonitorError { - fn from(err: io::Error) -> Self { - Self::Io(err) - } -} - -impl std::fmt::Display for SysMonitorError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Io(e) => write!(f, "I/O error: {e}"), - Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"), - Self::ParseError(s) => write!(f, "Failed to parse value: {s}"), - Self::ProcStatParseError(s) => { - write!(f, "Failed to parse /proc/stat: {s}") - } - Self::NotAvailable(s) => write!(f, "Information not available: {s}"), - } - } -} - impl std::error::Error for SysMonitorError {} pub type Result = std::result::Result; @@ -64,83 +38,15 @@ fn read_sysfs_value(path: impl AsRef) -> Result { }) } -pub fn get_system_info() -> Result { - let mut cpu_model = "Unknown".to_string(); - if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") { - for line in cpuinfo.lines() { - if line.starts_with("model name") { - if let Some(val) = line.split(':').nth(1) { - cpu_model = val.trim().to_string(); - break; - } - } - } - } - +pub fn get_system_info() -> SystemInfo { + 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(); - let mut linux_distribution = "Unknown".to_string(); - if let Ok(os_release) = fs::read_to_string("/etc/os-release") { - for line in os_release.lines() { - if line.starts_with("PRETTY_NAME=") { - if let Some(val) = line.split('=').nth(1) { - linux_distribution = val.trim_matches('"').to_string(); - break; - } - } - } - } else if let Ok(lsb_release) = fs::read_to_string("/etc/lsb-release") { - // fallback for some systems - for line in lsb_release.lines() { - if line.starts_with("DISTRIB_DESCRIPTION=") { - if let Some(val) = line.split('=').nth(1) { - linux_distribution = val.trim_matches('"').to_string(); - break; - } - } - } - } - - Ok(SystemInfo { + SystemInfo { cpu_model, architecture, linux_distribution, - }) -} - -fn get_logical_core_count() -> Result { - let mut count = 0; - let path = Path::new("/sys/devices/system/cpu"); - if path.exists() { - for entry in fs::read_dir(path)? { - let entry = entry?; - let name = entry.file_name(); - if let Some(name_str) = name.to_str() { - if name_str.starts_with("cpu") - && name_str.len() > 3 - && name_str[3..].chars().all(char::is_numeric) - { - // Check if it's a directory representing a core that can have cpufreq - if entry.path().join("cpufreq").exists() { - count += 1; - } else if Path::new(&format!("/sys/devices/system/cpu/{name_str}/online")) - .exists() - { - // Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads) - // This is a simplification; true cpufreq capability is key. - // If cpufreq dir doesn't exist, it might not be controllable by this tool. - // For counting purposes, we count it if it's an online CPU. - count += 1; - } - } - } - } - } - if count == 0 { - // Fallback to num_cpus crate if sysfs parsing fails or yields 0 - Ok(num_cpus::get() as u32) - } else { - Ok(count) } } @@ -474,7 +380,9 @@ pub fn get_all_cpu_core_info() -> Result> { thread::sleep(Duration::from_millis(250)); // Interval for CPU usage calculation let final_cpu_times = read_all_cpu_times()?; - let num_cores = get_logical_core_count()?; // Or derive from keys in cpu_times + let num_cores = get_logical_core_count() + .map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?; + let mut core_infos = Vec::with_capacity(num_cores as usize); for core_id in 0..num_cores { @@ -497,44 +405,38 @@ pub fn get_all_cpu_core_info() -> Result> { Ok(core_infos) } -pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { +pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // FIXME: Assume global settings can be read from cpu0 or are consistent. // This might not work properly for heterogeneous systems (e.g. big.LITTLE) - let cpufreq_base = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/"); - - let current_governor = if cpufreq_base.join("scaling_governor").exists() { - read_sysfs_file_trimmed(cpufreq_base.join("scaling_governor")).ok() + let cpufreq_base_path = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/"); + let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); + let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); + let current_governor = if cpufreq_base_path.join("scaling_governor").exists() { + read_sysfs_file_trimmed(cpufreq_base_path.join("scaling_governor")).ok() } else { None }; - let available_governors = if cpufreq_base.join("scaling_available_governors").exists() { - read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")).map_or_else(|_| vec![], |s| s.split_whitespace().map(String::from).collect()) - } else { - vec![] - }; + let available_governors = get_platform_profiles().unwrap_or_else(|_| vec![]); - let turbo_status = if Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo").exists() { + let turbo_status = if turbo_status_path.exists() { // 0 means turbo enabled, 1 means disabled for intel_pstate - read_sysfs_value::("/sys/devices/system/cpu/intel_pstate/no_turbo") + read_sysfs_value::(turbo_status_path) .map(|val| val == 0) .ok() - } else if Path::new("/sys/devices/system/cpu/cpufreq/boost").exists() { + } else if boost_path.exists() { // 1 means turbo enabled, 0 means disabled for generic cpufreq boost - read_sysfs_value::("/sys/devices/system/cpu/cpufreq/boost") - .map(|val| val == 1) - .ok() + read_sysfs_value::(boost_path).map(|val| val == 1).ok() } else { None }; - // EPP (Energy Performance Preference) let energy_perf_pref = - read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok(); + read_sysfs_file_trimmed(cpufreq_base_path.join("energy_performance_preference")).ok(); // EPB (Energy Performance Bias) let energy_perf_bias = - read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok(); + read_sysfs_file_trimmed(cpufreq_base_path.join("energy_performance_bias")).ok(); let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); let _platform_profile_choices = @@ -562,7 +464,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { } }; - Ok(CpuGlobalInfo { + CpuGlobalInfo { current_governor, available_governors, turbo_status, @@ -570,7 +472,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { epb: energy_perf_bias, platform_profile, average_temperature_celsius, - }) + } } pub fn get_battery_info(config: &AppConfig) -> Result> { @@ -581,10 +483,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { return Ok(batteries); // no power supply directory } - let ignored_supplies = config - .ignored_power_supplies - .clone() - .unwrap_or_default(); + let ignored_supplies = config.ignored_power_supplies.clone().unwrap_or_default(); // Determine overall AC connection status let mut overall_ac_connected = false; @@ -701,9 +600,9 @@ pub fn get_system_load() -> Result { } pub fn collect_system_report(config: &AppConfig) -> Result { - let system_info = get_system_info()?; + let system_info = get_system_info(); let cpu_cores = get_all_cpu_core_info()?; - let cpu_global = get_cpu_global_info(&cpu_cores)?; + let cpu_global = get_cpu_global_info(&cpu_cores); let batteries = get_battery_info(config)?; let system_load = get_system_load()?; @@ -716,3 +615,64 @@ pub fn collect_system_report(config: &AppConfig) -> Result { timestamp: SystemTime::now(), }) } + +pub fn get_cpu_model() -> Result { + let path = Path::new("/proc/cpuinfo"); + let content = fs::read_to_string(path).map_err(|_| { + SysMonitorError::ReadError(format!("Cannot read contents of {}.", path.display())) + })?; + + for line in content.lines() { + if line.starts_with("model name") { + if let Some(val) = line.split(':').nth(1) { + let cpu_model = val.trim().to_string(); + return Ok(cpu_model); + } + } + } + Err(SysMonitorError::ParseError( + "Could not find CPU model name in /proc/cpuinfo.".to_string(), + )) +} + +pub fn get_linux_distribution() -> Result { + 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() + ))) +} diff --git a/src/util/error.rs b/src/util/error.rs new file mode 100644 index 0000000..1dc49a9 --- /dev/null +++ b/src/util/error.rs @@ -0,0 +1,69 @@ +use std::io; + +#[derive(Debug)] +pub enum ControlError { + Io(io::Error), + WriteError(String), + InvalidValueError(String), + NotSupported(String), + PermissionDenied(String), + InvalidProfile(String), +} + +impl From for ControlError { + fn from(err: io::Error) -> Self { + match err.kind() { + io::ErrorKind::PermissionDenied => Self::PermissionDenied(err.to_string()), + _ => Self::Io(err), + } + } +} + +impl std::fmt::Display for ControlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::WriteError(s) => write!(f, "Failed to write to sysfs path: {s}"), + Self::InvalidValueError(s) => write!(f, "Invalid value for setting: {s}"), + Self::NotSupported(s) => write!(f, "Control action not supported: {s}"), + Self::PermissionDenied(s) => { + write!(f, "Permission denied: {s}. Try running with sudo.") + } + Self::InvalidProfile(s) => { + write!( + f, + "Invalid platform control profile {s} supplied, please provide a valid one." + ) + } + } + } +} + +#[derive(Debug)] +pub enum SysMonitorError { + Io(io::Error), + ReadError(String), + ParseError(String), + ProcStatParseError(String), + NotAvailable(String), +} + +impl From for SysMonitorError { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +impl std::fmt::Display for SysMonitorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"), + Self::ParseError(s) => write!(f, "Failed to parse value: {s}"), + Self::ProcStatParseError(s) => { + write!(f, "Failed to parse /proc/stat: {s}") + } + Self::NotAvailable(s) => write!(f, "Information not available: {s}"), + } + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..a91e735 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod error;