diff --git a/Cargo.lock b/Cargo.lock index 5f41fc6..2b0446d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.38" @@ -131,6 +141,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "ctrlc" version = "3.4.7" @@ -141,6 +160,28 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "dirs" version = "6.0.0" @@ -453,7 +494,9 @@ version = "0.3.2" dependencies = [ "anyhow", "clap", + "clap-verbosity-flag", "ctrlc", + "derive_more", "dirs", "env_logger", "jiff", @@ -462,6 +505,7 @@ dependencies = [ "serde", "thiserror", "toml", + "yansi", ] [[package]] @@ -542,6 +586,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -635,3 +691,9 @@ checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 69f9617..3276b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,6 @@ env_logger = "0.11" thiserror = "2.0" anyhow = "1.0" jiff = "0.2.13" +clap-verbosity-flag = "3.0.2" +yansi = "1.0.1" +derive_more = { version = "2.0.1", features = ["full"] } diff --git a/src/battery.rs b/src/battery.rs index 8fe75dd..b15e6bb 100644 --- a/src/battery.rs +++ b/src/battery.rs @@ -1,5 +1,4 @@ use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs}; -use log::{debug, warn}; use std::{ fs, io, path::{Path, PathBuf}, @@ -118,7 +117,7 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result e, Err(e) => { - warn!("Failed to read power-supply entry: {e}"); + log::warn!("Failed to read power-supply entry: {e}"); continue; } }; @@ -131,16 +130,17 @@ fn find_supported_batteries(power_supply_path: &Path) -> Result { - debug!( + log::debug!( "Set {}-{}% charge thresholds for {} battery '{}'", - start_threshold, stop_threshold, battery.pattern.description, battery.name + start_threshold, + stop_threshold, + battery.pattern.description, + battery.name ); success_count += 1; } @@ -184,14 +187,16 @@ fn apply_thresholds_to_batteries( if let Some(prev_stop) = ¤t_stop { let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop); if let Err(re) = restore_result { - warn!( + log::warn!( "Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.", - battery.name, re + battery.name, + re ); } else { - debug!( + log::debug!( "Restored previous stop threshold ({}) for battery '{}'", - prev_stop, battery.name + prev_stop, + battery.name ); } } @@ -212,7 +217,7 @@ fn apply_thresholds_to_batteries( if success_count > 0 { if !errors.is_empty() { - warn!( + log::warn!( "Partial success setting battery thresholds: {}", errors.join("; ") ); diff --git a/src/core.rs b/src/core.rs index 76dc940..07581aa 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,31 +1,3 @@ -use clap::ValueEnum; -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, ValueEnum)] -pub enum TurboSetting { - Always, // turbo is forced on (if possible) - Auto, // system or driver controls turbo - Never, // turbo is forced off -} - -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum GovernorOverrideMode { - Performance, - Powersave, - Reset, -} - -impl fmt::Display for GovernorOverrideMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Performance => write!(f, "performance"), - Self::Powersave => write!(f, "powersave"), - Self::Reset => write!(f, "reset"), - } - } -} - pub struct SystemInfo { // Overall system details pub cpu_model: String, diff --git a/src/cpu.rs b/src/cpu.rs index 5629df3..0f5f304 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,479 +1,321 @@ -use crate::core::{GovernorOverrideMode, TurboSetting}; -use crate::util::error::ControlError; -use core::str; -use log::debug; +use anyhow::{Context, bail}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; + use std::{fs, io, path::Path, string::ToString}; -pub type Result = std::result::Result; +// // Valid EPP (Energy Performance Preference) string values. +// const EPP_FALLBACK_VALUES: &[&str] = &[ +// "default", +// "performance", +// "balance-performance", +// "balance_performance", // Alternative form with underscore. +// "balance-power", +// "balance_power", // Alternative form with underscore. +// "power", +// ]; -// Valid EPB string values -const VALID_EPB_STRINGS: &[&str] = &[ - "performance", - "balance-performance", - "balance_performance", // alternative form - "balance-power", - "balance_power", // alternative form - "power", -]; +fn exists(path: impl AsRef) -> bool { + let path = path.as_ref(); -// EPP (Energy Performance Preference) string values -const EPP_FALLBACK_VALUES: &[&str] = &[ - "default", - "performance", - "balance-performance", - "balance_performance", // alternative form with underscore - "balance-power", - "balance_power", // alternative form with underscore - "power", -]; + path.exists() +} -// Write a value to a sysfs file -fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<()> { - let p = path.as_ref(); +// Not doing any anyhow stuff here as all the calls of this ignore errors. +fn read_u64(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); - fs::write(p, value).map_err(|e| { - let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e); - match e.kind() { - io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), - io::ErrorKind::NotFound => { - ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) - } - _ => ControlError::WriteError(error_msg), - } + let content = fs::read_to_string(path)?; + + Ok(content.trim().parse::()?) +} + +fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { + let path = path.as_ref(); + + fs::write(path, value).with_context(|| { + format!( + "failed to write '{value}' to '{path}'", + path = path.display(), + ) }) } -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 num_cores: u32 = 0; - let path = Path::new("/sys/devices/system/cpu"); - if !path.exists() { - return Err(ControlError::NotSupported(format!( - "No logical cores found at {}.", - path.display() - ))); - } +/// Get real, tunable CPUs. +pub fn get_real_cpus() -> anyhow::Result> { + const PATH: &str = "/sys/devices/system/cpu"; - let entries = fs::read_dir(path) - .map_err(|_| { - ControlError::PermissionDenied(format!("Cannot read contents of {}.", path.display())) - })? - .flatten(); + let mut cpus = vec![]; - for entry in entries { + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read contents of '{PATH}'"))? + .flatten() + { let entry_file_name = entry.file_name(); + let Some(name) = entry_file_name.to_str() else { continue; }; - // Skip non-CPU directories (e.g., cpuidle, cpufreq) - if !name.starts_with("cpu") || name.len() <= 3 || !name[3..].chars().all(char::is_numeric) { + let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { continue; - } + }; + // Has to match "cpu{N}". + let Ok(cpu) = cpu_prefix_removed.parse::() else { + continue; + }; + + // Has to match "cpu{N}/cpufreq". if !entry.path().join("cpufreq").exists() { continue; } - if name[3..].parse::().is_ok() { - num_cores += 1; - } - } - if num_cores == 0 { - // Fallback if sysfs iteration above fails to find any cpufreq cores - num_cores = num_cpus::get() as u32; + cpus.push(cpu); } - Ok(num_cores) + // Fall back if sysfs iteration above fails to find any cpufreq CPUs. + if cpus.is_empty() { + cpus = (0..num_cpus::get() as u32).collect(); + } + + Ok(cpus) } -fn for_each_cpu_core(mut action: F) -> Result<()> -where - F: FnMut(u32) -> Result<()>, -{ - let num_cores: u32 = get_logical_core_count()?; +/// Set the governor for a CPU. +pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> { + let governors = get_available_governors_for(cpu); - for core_id in 0u32..num_cores { - action(core_id)?; + if !governors + .iter() + .any(|avail_governor| avail_governor == governor) + { + bail!( + "governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}", + governors = governors.join(", "), + ); } - Ok(()) + + write( + format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor"), + governor, + ) + .with_context(|| { + format!( + "this probably means that CPU {cpu} doesn't exist or doesn't support changing governors" + ) + }) } -pub fn set_governor(governor: &str, core_id: Option) -> Result<()> { - // Validate the governor is available on this system - // This returns both the validation result and the list of available governors - let (is_valid, available_governors) = is_governor_valid(governor)?; - - if !is_valid { - return Err(ControlError::InvalidGovernor(format!( - "Governor '{}' is not available on this system. Valid governors: {}", - governor, - available_governors.join(", ") - ))); - } - - let action = |id: u32| { - let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_governor"); - if Path::new(&path).exists() { - write_sysfs_value(&path, governor) - } else { - // Silently ignore if the path doesn't exist for a specific core, - // as not all cores might have cpufreq (e.g. offline cores) - Ok(()) - } +/// Get available CPU governors for a CPU. +fn get_available_governors_for(cpu: u32) -> Vec { + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_available_governors" + )) else { + return Vec::new(); }; - core_id.map_or_else(|| for_each_cpu_core(action), action) + content + .split_whitespace() + .map(ToString::to_string) + .collect() } -/// Check if the provided governor is available in the system -/// Returns a tuple of (`is_valid`, `available_governors`) to avoid redundant file reads -fn is_governor_valid(governor: &str) -> Result<(bool, Vec)> { - let governors = get_available_governors()?; - - // Convert input governor to lowercase for case-insensitive comparison - let governor_lower = governor.to_lowercase(); - - // Convert all available governors to lowercase for comparison - let governors_lower: Vec = governors.iter().map(|g| g.to_lowercase()).collect(); - - // Check if the lowercase governor is in the lowercase list - Ok((governors_lower.contains(&governor_lower), governors)) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] +pub enum Turbo { + Always, + Never, } -/// Get available CPU governors from the system -fn get_available_governors() -> Result> { - let cpu_base_path = Path::new("/sys/devices/system/cpu"); - - // First try the traditional path with cpu0. This is the most common case - // and will usually catch early, but we should try to keep the code to handle - // "edge" cases lightweight, for the (albeit smaller) number of users that - // run Superfreq on unusual systems. - let cpu0_path = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors"; - if Path::new(cpu0_path).exists() { - let content = fs::read_to_string(cpu0_path).map_err(|e| { - ControlError::ReadError(format!("Failed to read available governors from cpu0: {e}")) - })?; - - let governors: Vec = content - .split_whitespace() - .map(ToString::to_string) - .collect(); - - if !governors.is_empty() { - return Ok(governors); - } - } - - // If cpu0 doesn't have the file or it's empty, scan all CPUs - // This handles heterogeneous systems where cpu0 might not have cpufreq - if let Ok(entries) = fs::read_dir(cpu_base_path) { - for entry in entries.flatten() { - let path = entry.path(); - let file_name = entry.file_name(); - let name = match file_name.to_str() { - Some(name) => name, - None => continue, - }; - - // Skip non-CPU directories - if !name.starts_with("cpu") - || name.len() <= 3 - || !name[3..].chars().all(char::is_numeric) - { - continue; - } - - let governor_path = path.join("cpufreq/scaling_available_governors"); - if governor_path.exists() { - match fs::read_to_string(&governor_path) { - Ok(content) => { - let governors: Vec = content - .split_whitespace() - .map(ToString::to_string) - .collect(); - - if !governors.is_empty() { - return Ok(governors); - } - } - Err(_) => continue, // try next CPU if this one fails - } - } - } - } - - // If we get here, we couldn't find any valid governors list - Err(ControlError::NotSupported( - "Could not determine available governors on any CPU".to_string(), - )) -} - -pub fn set_turbo(setting: TurboSetting) -> Result<()> { - let value_pstate = match setting { - TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled - TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled - // Auto mode is handled at the engine level, not directly at the sysfs level - TurboSetting::Auto => { - debug!("Turbo Auto mode is managed by engine logic based on system conditions"); - return Ok(()); - } - }; +pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> { let value_boost = match setting { - TurboSetting::Always => "1", // boost = 1 means turbo is enabled - TurboSetting::Never => "0", // boost = 0 means turbo is disabled - TurboSetting::Auto => { - debug!("Turbo Auto mode is managed by engine logic based on system conditions"); - return Ok(()); - } + Turbo::Always => "1", // boost = 1 means turbo is enabled. + Turbo::Never => "0", // boost = 0 means turbo is disabled. + }; + + let value_boost_negated = match setting { + Turbo::Always => "0", // no_turbo = 0 means turbo is enabled. + Turbo::Never => "1", // no_turbo = 1 means turbo is disabled. }; // AMD specific paths - let amd_pstate_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; + let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; // Path priority (from most to least specific) - let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"; - let boost_path = "/sys/devices/system/cpu/cpufreq/boost"; + let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo"; + let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; // Try each boost control path in order of specificity - if Path::new(pstate_path).exists() { - write_sysfs_value(pstate_path, value_pstate) - } else if Path::new(amd_pstate_path).exists() { - write_sysfs_value(amd_pstate_path, value_boost) - } else if Path::new(msr_boost_path).exists() { - write_sysfs_value(msr_boost_path, value_boost) - } else if Path::new(boost_path).exists() { - write_sysfs_value(boost_path, value_boost) - } else { - // Also try per-core cpufreq boost for some AMD systems - let result = try_set_per_core_boost(value_boost)?; - if result { - Ok(()) - } else { - Err(ControlError::NotSupported( - "No supported CPU boost control mechanism found.".to_string(), - )) - } + if write(intel_boost_path_negated, value_boost_negated).is_ok() { + return Ok(()); } -} - -/// Try to set boost on a per-core basis for systems that support it -fn try_set_per_core_boost(value: &str) -> Result { - let mut success = false; - let num_cores = get_logical_core_count()?; - - for core_id in 0..num_cores { - let boost_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/boost"); - - if Path::new(&boost_path).exists() { - write_sysfs_value(&boost_path, value)?; - success = true; - } + if write(amd_boost_path, value_boost).is_ok() { + return Ok(()); + } + if write(msr_boost_path, value_boost).is_ok() { + return Ok(()); + } + if write(generic_boost_path, value_boost).is_ok() { + return Ok(()); } - Ok(success) + // Also try per-core cpufreq boost for some AMD systems. + if get_real_cpus()?.iter().any(|cpu| { + write( + &format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/boost"), + value_boost, + ) + .is_ok() + }) { + return Ok(()); + } + + bail!("no supported CPU boost control mechanism found"); } -pub fn set_epp(epp: &str, core_id: Option) -> Result<()> { +pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> { // Validate the EPP value against available options - let available_epp = get_available_epp_values()?; - if !available_epp.iter().any(|v| v.eq_ignore_ascii_case(epp)) { - return Err(ControlError::InvalidValueError(format!( - "Invalid EPP value: '{}'. Available values: {}", - epp, - available_epp.join(", ") - ))); + let epps = get_available_epps(cpu); + + if !epps.iter().any(|avail_epp| avail_epp == epp) { + bail!( + "epp value '{epp}' is not availabile for CPU {cpu}. valid epp values: {epps}", + epps = epps.join(", "), + ); } - let action = |id: u32| { - let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_preference"); - if Path::new(&path).exists() { - write_sysfs_value(&path, epp) - } else { - Ok(()) - } - }; - core_id.map_or_else(|| for_each_cpu_core(action), action) + write( + format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_preference"), + epp, + ) + .with_context(|| { + format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPP") + }) } -/// Get available EPP values from the system -fn get_available_epp_values() -> Result> { - let path = "/sys/devices/system/cpu/cpu0/cpufreq/energy_performance_available_preferences"; +/// Get available EPP values for a CPU. +fn get_available_epps(cpu: u32) -> Vec { + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_available_preferences" + )) else { + return Vec::new(); + }; - if !Path::new(path).exists() { - // If the file doesn't exist, fall back to a default set of common values - // This is safer than failing outright, as some systems may allow these values │ - // even without explicitly listing them - return Ok(EPP_FALLBACK_VALUES.iter().map(|&s| s.to_string()).collect()); - } - - let content = fs::read_to_string(path).map_err(|e| { - ControlError::ReadError(format!("Failed to read available EPP values: {e}")) - })?; - - Ok(content + content .split_whitespace() .map(ToString::to_string) - .collect()) + .collect() } -pub fn set_epb(epb: &str, core_id: Option) -> Result<()> { - // Validate EPB value - should be a number 0-15 or a recognized string value +pub fn set_epb(epb: &str, cpu: u32) -> anyhow::Result<()> { + // Validate EPB value - should be a number 0-15 or a recognized string value. validate_epb_value(epb)?; - let action = |id: u32| { - let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/energy_performance_bias"); - if Path::new(&path).exists() { - write_sysfs_value(&path, epb) - } else { - Ok(()) - } - }; - core_id.map_or_else(|| for_each_cpu_core(action), action) + write( + format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/energy_performance_bias"), + epb, + ) + .with_context(|| { + format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing EPB") + }) } -fn validate_epb_value(epb: &str) -> Result<()> { - // EPB can be a number from 0-15 or a recognized string - // Try parsing as a number first +fn validate_epb_value(epb: &str) -> anyhow::Result<()> { + // EPB can be a number from 0-15 or a recognized string. + + const VALID_EPB_STRINGS: &[&str] = &[ + "performance", + "balance-performance", + "balance_performance", // Alternative form with underscore. + "balance-power", + "balance_power", // Alternative form with underscore. + "power", + ]; + + // Try parsing as a number first. if let Ok(value) = epb.parse::() { if value <= 15 { return Ok(()); } - return Err(ControlError::InvalidValueError(format!( - "EPB numeric value must be between 0 and 15, got {value}" - ))); + + bail!("EPB numeric value must be between 0 and 15, got {value}"); } // If not a number, check if it's a recognized string value. - // This is using case-insensitive comparison - if VALID_EPB_STRINGS - .iter() - .any(|valid| valid.eq_ignore_ascii_case(epb)) - { - Ok(()) - } else { - Err(ControlError::InvalidValueError(format!( - "Invalid EPB value: '{}'. Must be a number 0-15 or one of: {}", - epb, - VALID_EPB_STRINGS.join(", ") - ))) - } -} - -pub fn set_min_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { - // Check if the new minimum frequency would be greater than current maximum - if let Some(id) = core_id { - validate_min_frequency(id, freq_mhz)?; - } else { - // Check for all cores - let num_cores = get_logical_core_count()?; - for id in 0..num_cores { - validate_min_frequency(id, freq_mhz)?; - } - } - - // XXX: We use u64 for the intermediate calculation to prevent overflow - let freq_khz = u64::from(freq_mhz) * 1000; - let freq_khz_str = freq_khz.to_string(); - - let action = |id: u32| { - let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_min_freq"); - if Path::new(&path).exists() { - write_sysfs_value(&path, &freq_khz_str) - } else { - Ok(()) - } - }; - core_id.map_or_else(|| for_each_cpu_core(action), action) -} - -pub fn set_max_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { - // Check if the new maximum frequency would be less than current minimum - if let Some(id) = core_id { - validate_max_frequency(id, freq_mhz)?; - } else { - // Check for all cores - let num_cores = get_logical_core_count()?; - for id in 0..num_cores { - validate_max_frequency(id, freq_mhz)?; - } - } - - // XXX: Use a u64 here as well. - let freq_khz = u64::from(freq_mhz) * 1000; - let freq_khz_str = freq_khz.to_string(); - - let action = |id: u32| { - let path = format!("/sys/devices/system/cpu/cpu{id}/cpufreq/scaling_max_freq"); - if Path::new(&path).exists() { - write_sysfs_value(&path, &freq_khz_str) - } else { - Ok(()) - } - }; - core_id.map_or_else(|| for_each_cpu_core(action), action) -} - -fn read_sysfs_value_as_u32(path: &str) -> Result { - if !Path::new(path).exists() { - return Err(ControlError::NotSupported(format!( - "File does not exist: {path}" - ))); - } - - let content = fs::read_to_string(path) - .map_err(|e| ControlError::ReadError(format!("Failed to read {path}: {e}")))?; - - content - .trim() - .parse::() - .map_err(|e| ControlError::ParseError(format!("Failed to parse value from {path}: {e}"))) -} - -fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> { - let max_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_max_freq"); - - if !Path::new(&max_freq_path).exists() { + if VALID_EPB_STRINGS.contains(&epb) { return Ok(()); } - let max_freq_khz = read_sysfs_value_as_u32(&max_freq_path)?; - let new_min_freq_khz = new_min_freq_mhz * 1000; + bail!( + "invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}", + valid = VALID_EPB_STRINGS.join(", "), + ); +} - if new_min_freq_khz > max_freq_khz { - return Err(ControlError::InvalidValueError(format!( - "Minimum frequency ({} MHz) cannot be higher than maximum frequency ({} MHz) for core {}", - new_min_freq_mhz, - max_freq_khz / 1000, - core_id - ))); +pub fn set_frequency_minimum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { + validate_frequency_minimum(frequency_mhz, cpu)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = u64::from(frequency_mhz) * 1000; + let frequency_khz = frequency_khz.to_string(); + + write( + format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing minimum frequency") + }) +} + +pub fn set_frequency_maximum(frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { + validate_max_frequency(frequency_mhz, cpu)?; + + // We use u64 for the intermediate calculation to prevent overflow + let frequency_khz = u64::from(frequency_mhz) * 1000; + let frequency_khz = frequency_khz.to_string(); + + write( + format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_max_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {cpu} doesn't exist or doesn't support changing maximum frequency") + }) +} + +fn validate_frequency_minimum(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { + let Ok(minimum_frequency_khz) = read_u64(format!( + "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq" + )) else { + // Just let it pass if we can't find anything. + return Ok(()); + }; + + if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz { + bail!( + "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for CPU {cpu}", + minimum_frequency_khz / 1000, + ); } Ok(()) } -fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> { - let min_freq_path = format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/scaling_min_freq"); - - if !Path::new(&min_freq_path).exists() { +fn validate_max_frequency(new_frequency_mhz: u64, cpu: u32) -> anyhow::Result<()> { + let Ok(maximum_frequency_khz) = read_u64(format!( + "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_min_freq" + )) else { + // Just let it pass if we can't find anything. return Ok(()); - } + }; - let min_freq_khz = read_sysfs_value_as_u32(&min_freq_path)?; - let new_max_freq_khz = new_max_freq_mhz * 1000; - - if new_max_freq_khz < min_freq_khz { - return Err(ControlError::InvalidValueError(format!( - "Maximum frequency ({} MHz) cannot be lower than minimum frequency ({} MHz) for core {}", - new_max_freq_mhz, - min_freq_khz / 1000, - core_id - ))); + if new_frequency_mhz * 1000 > maximum_frequency_khz { + bail!( + "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for CPU {cpu}", + maximum_frequency_khz / 1000, + ); } Ok(()) @@ -485,137 +327,108 @@ fn validate_max_frequency(core_id: u32, new_max_freq_mhz: u32) -> Result<()> { /// Also see [`The Kernel docs`] for this. /// /// [`The Kernel docs`]: -/// -/// # Examples -/// -/// ``` -/// set_platform_profile("balanced"); -/// ``` -/// -pub fn set_platform_profile(profile: &str) -> Result<()> { - let path = "/sys/firmware/acpi/platform_profile"; - if !Path::new(path).exists() { - return Err(ControlError::NotSupported(format!( - "Platform profile control not found at {path}.", - ))); +pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { + let profiles = get_platform_profiles(); + + if !profiles + .iter() + .any(|avail_profile| avail_profile == profile) + { + bail!( + "profile '{profile}' is not available for system. valid profiles: {profiles}", + profiles = profiles.join(", "), + ); } - let available_profiles = get_platform_profiles()?; - - if !available_profiles.contains(&profile.to_string()) { - return Err(ControlError::InvalidProfile(format!( - "Invalid platform control profile provided.\n\ - Provided profile: {} \n\ - Available profiles:\n\ - {}", - profile, - available_profiles.join(", ") - ))); - } - write_sysfs_value(path, profile) + write("/sys/firmware/acpi/platform_profile", profile) + .context("this probably means that your system does not support changing ACPI profiles") } -/// Returns the list of available platform profiles. -/// -/// # Errors -/// -/// # Returns -/// -/// - [`ControlError::NotSupported`] if: -/// - The file `/sys/firmware/acpi/platform_profile_choices` does not exist. -/// - The file `/sys/firmware/acpi/platform_profile_choices` is empty. -/// -/// - [`ControlError::PermissionDenied`] if the file `/sys/firmware/acpi/platform_profile_choices` cannot be read. -/// -pub fn get_platform_profiles() -> Result> { +/// Get the list of available platform profiles. +pub fn get_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - if !Path::new(path).exists() { - return Err(ControlError::NotSupported(format!( - "Platform profile choices not found at {path}." - ))); - } + let Ok(content) = fs::read_to_string(path) else { + return Vec::new(); + }; - let content = fs::read_to_string(path) - .map_err(|_| ControlError::PermissionDenied(format!("Cannot read contents of {path}.")))?; - - Ok(content + content .split_whitespace() .map(ToString::to_string) - .collect()) + .collect() } -/// Path for storing the governor override state +/// Path for storing the governor override state. const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override"; -/// Force a specific CPU governor or reset to automatic mode -pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> { - // Create directory if it doesn't exist - let dir_path = Path::new("/etc/xdg/superfreq"); - if !dir_path.exists() { - fs::create_dir_all(dir_path).map_err(|e| { - if e.kind() == io::ErrorKind::PermissionDenied { - ControlError::PermissionDenied(format!( - "Permission denied creating directory: {}. Try running with sudo.", - dir_path.display() - )) - } else { - ControlError::Io(e) - } +#[derive(Display, Debug, Clone, Copy, clap::ValueEnum)] +pub enum GovernorOverride { + #[display("performance")] + Performance, + #[display("powersave")] + Powersave, + #[display("reset")] + Reset, +} + +pub fn set_governor_override(mode: GovernorOverride) -> anyhow::Result<()> { + let parent = Path::new(GOVERNOR_OVERRIDE_PATH).parent().unwrap(); + if !parent.exists() { + fs::create_dir_all(parent).with_context(|| { + format!( + "failed to create directory '{path}'", + path = parent.display(), + ) })?; } match mode { - GovernorOverrideMode::Reset => { + GovernorOverride::Reset => { // Remove the override file if it exists - if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { - fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| { - if e.kind() == io::ErrorKind::PermissionDenied { - ControlError::PermissionDenied(format!( - "Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." - )) - } else { - ControlError::Io(e) - } - })?; - println!( - "Governor override has been reset. Normal profile-based settings will be used." - ); - } else { - println!("No governor override was set."); - } - Ok(()) - } - GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => { - // Create the override file with the selected governor - let governor = mode.to_string().to_lowercase(); - fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| { - if e.kind() == io::ErrorKind::PermissionDenied { - ControlError::PermissionDenied(format!( - "Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." - )) - } else { - ControlError::Io(e) + let result = fs::remove_file(GOVERNOR_OVERRIDE_PATH); + + if let Err(error) = result { + if error.kind() != io::ErrorKind::NotFound { + return Err(error).with_context(|| { + format!( + "failed to delete governor override file '{GOVERNOR_OVERRIDE_PATH}'" + ) + }); } - })?; + } - // Also apply the governor immediately - set_governor(&governor, None)?; - - println!( - "Governor override set to '{governor}'. This setting will persist across reboots." + log::info!( + "governor override has been deleted. normal profile-based settings will be used" ); - println!("To reset, use: superfreq force-governor reset"); - Ok(()) + } + + GovernorOverride::Performance | GovernorOverride::Powersave => { + let governor = mode.to_string(); + + write(GOVERNOR_OVERRIDE_PATH, &governor) + .context("failed to write governor override")?; + + // TODO: Apply the setting too. + + log::info!( + "governor override set to '{governor}'. this setting will persist across reboots" + ); + log::info!("to reset, run: superfreq set --governor-persist reset"); } } + + Ok(()) } -/// Get the current governor override if set -pub fn get_governor_override() -> Option { - if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { - fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok() - } else { - None +/// Get the current governor override if set. +pub fn get_governor_override() -> anyhow::Result> { + match fs::read_to_string(GOVERNOR_OVERRIDE_PATH) { + Ok(governor_override) => Ok(Some(governor_override)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => Err(error).with_context(|| { + format!("failed to read governor override at '{GOVERNOR_OVERRIDE_PATH}'") + }), } } diff --git a/src/daemon.rs b/src/daemon.rs index dd90884..e2e4fb1 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -3,7 +3,6 @@ use crate::core::SystemReport; use crate::engine; use crate::monitor; use crate::util::error::{AppError, ControlError}; -use log::{LevelFilter, debug, error, info, warn}; use std::collections::VecDeque; use std::fs::File; use std::io::Write; @@ -99,7 +98,7 @@ fn compute_new( if idle_time_seconds > 0 { let idle_factor = idle_multiplier(idle_time_seconds); - debug!( + log::debug!( "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", idle_time_seconds, (idle_time_seconds as f32 / 60.0).round(), @@ -226,7 +225,7 @@ impl SystemHistory { > 15.0) { self.last_user_activity = Instant::now(); - debug!("User activity detected based on CPU usage"); + log::debug!("User activity detected based on CPU usage"); } } } @@ -245,7 +244,7 @@ impl SystemHistory { if temp_change > 5.0 { // 5°C rise in temperature self.last_user_activity = Instant::now(); - debug!("User activity detected based on temperature change"); + log::debug!("User activity detected based on temperature change"); } } } @@ -302,7 +301,7 @@ impl SystemHistory { // State changes (except to Idle) likely indicate user activity if new_state != SystemState::Idle && new_state != SystemState::LowLoad { self.last_user_activity = Instant::now(); - debug!("User activity detected based on system state change to {new_state:?}"); + log::debug!("User activity detected based on system state change to {new_state:?}"); } // Update state @@ -313,7 +312,7 @@ impl SystemHistory { // Check for significant load changes if report.system_load.load_avg_1min > 1.0 { self.last_user_activity = Instant::now(); - debug!("User activity detected based on system load"); + log::debug!("User activity detected based on system load"); } } @@ -402,26 +401,8 @@ fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), C } /// Run the daemon -pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { - // Set effective log level based on config and verbose flag - let effective_log_level = if verbose { - LogLevel::Debug - } else { - config.daemon.log_level - }; - - // Get the appropriate level filter - let level_filter = match effective_log_level { - LogLevel::Error => LevelFilter::Error, - LogLevel::Warning => LevelFilter::Warn, - LogLevel::Info => LevelFilter::Info, - LogLevel::Debug => LevelFilter::Debug, - }; - - // Update the log level filter if needed, without re-initializing the logger - log::set_max_level(level_filter); - - info!("Starting superfreq daemon..."); +pub fn run_daemon(config: AppConfig) -> Result<(), AppError> { + log::info!("Starting superfreq daemon..."); // Validate critical configuration values before proceeding if let Err(err) = validate_poll_intervals( @@ -437,26 +418,28 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { // Set up signal handlers ctrlc::set_handler(move || { - info!("Received shutdown signal, exiting..."); + log::info!("Received shutdown signal, exiting..."); r.store(false, Ordering::SeqCst); }) .map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?; - info!( + log::info!( "Daemon initialized with poll interval: {}s", config.daemon.poll_interval_sec ); // Set up stats file if configured if let Some(stats_path) = &config.daemon.stats_file_path { - info!("Stats will be written to: {stats_path}"); + log::info!("Stats will be written to: {stats_path}"); } // Variables for adaptive polling // Make sure that the poll interval is *never* zero to prevent a busy loop let mut current_poll_interval = config.daemon.poll_interval_sec.max(1); if config.daemon.poll_interval_sec == 0 { - warn!("Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"); + log::warn!( + "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" + ); } let mut system_history = SystemHistory::default(); @@ -466,7 +449,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { match monitor::collect_system_report(&config) { Ok(report) => { - debug!("Collected system report, applying settings..."); + log::debug!("Collected system report, applying settings..."); // Store the current state before updating history let previous_state = system_history.current_state.clone(); @@ -477,24 +460,24 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { // Update the stats file if configured if let Some(stats_path) = &config.daemon.stats_file_path { if let Err(e) = write_stats_file(stats_path, &report) { - error!("Failed to write stats file: {e}"); + log::error!("Failed to write stats file: {e}"); } } match engine::determine_and_apply_settings(&report, &config, None) { Ok(()) => { - debug!("Successfully applied system settings"); + log::debug!("Successfully applied system settings"); // If system state changed, log the new state if system_history.current_state != previous_state { - info!( + log::info!( "System state changed to: {:?}", system_history.current_state ); } } Err(e) => { - error!("Error applying system settings: {e}"); + log::error!("Error applying system settings: {e}"); } } @@ -509,7 +492,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { // Store the new interval system_history.last_computed_interval = Some(optimal_interval); - debug!("Recalculated optimal interval: {optimal_interval}s"); + log::debug!("Recalculated optimal interval: {optimal_interval}s"); // Don't change the interval too dramatically at once match optimal_interval.cmp(¤t_poll_interval) { @@ -528,7 +511,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { } Err(e) => { // Log the error and stop the daemon when an invalid configuration is detected - error!("Critical configuration error: {e}"); + log::error!("Critical configuration error: {e}"); running.store(false, Ordering::SeqCst); break; } @@ -540,7 +523,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { config.daemon.max_poll_interval_sec, ); - debug!("Adaptive polling: set interval to {current_poll_interval}s"); + log::debug!("Adaptive polling: set interval to {current_poll_interval}s"); } else { // If adaptive polling is disabled, still apply battery-saving adjustment if config.daemon.throttle_on_battery && on_battery { @@ -552,20 +535,22 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { current_poll_interval = (safe_interval * battery_multiplier) .min(config.daemon.max_poll_interval_sec); - debug!( + log::debug!( "On battery power, increased poll interval to {current_poll_interval}s" ); } else { // Use the configured poll interval current_poll_interval = config.daemon.poll_interval_sec.max(1); if config.daemon.poll_interval_sec == 0 { - debug!("Using minimum poll interval of 1s instead of configured 0s"); + log::debug!( + "Using minimum poll interval of 1s instead of configured 0s" + ); } } } } Err(e) => { - error!("Error collecting system report: {e}"); + log::error!("Error collecting system report: {e}"); } } @@ -574,12 +559,12 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), AppError> { let poll_duration = Duration::from_secs(current_poll_interval); if elapsed < poll_duration { let sleep_time = poll_duration - elapsed; - debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); + log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); std::thread::sleep(sleep_time); } } - info!("Daemon stopped"); + log::info!("Daemon stopped"); Ok(()) } diff --git a/src/engine.rs b/src/engine.rs index bbefc86..fadff3b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,6 @@ use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self}; use crate::util::error::{ControlError, EngineError}; -use log::{debug, info, warn}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -128,13 +127,13 @@ fn try_apply_feature( where F: FnOnce() -> Result, { - info!("Setting {feature_name} to '{value_description}'"); + log::info!("Setting {feature_name} to '{value_description}'"); match apply_fn() { Ok(_) => Ok(()), Err(e) => { if matches!(e, ControlError::NotSupported(_)) { - warn!( + log::warn!( "{feature_name} setting is not supported on this system. Skipping {feature_name} configuration." ); Ok(()) @@ -155,7 +154,7 @@ pub fn determine_and_apply_settings( ) -> Result<(), EngineError> { // First, check if there's a governor override set if let Some(override_governor) = cpu::get_governor_override() { - info!( + log::info!( "Governor override is active: '{}'. Setting governor.", override_governor.trim() ); @@ -182,35 +181,35 @@ pub fn determine_and_apply_settings( if let Some(mode) = force_mode { match mode { OperationalMode::Powersave => { - info!("Forced Powersave mode selected. Applying 'battery' profile."); + log::info!("Forced Powersave mode selected. Applying 'battery' profile."); selected_profile_config = &config.battery; } OperationalMode::Performance => { - info!("Forced Performance mode selected. Applying 'charger' profile."); + log::info!("Forced Performance mode selected. Applying 'charger' profile."); selected_profile_config = &config.charger; } } } else { // Use the previously computed on_ac_power value if on_ac_power { - info!("On AC power, selecting Charger profile."); + log::info!("On AC power, selecting Charger profile."); selected_profile_config = &config.charger; } else { - info!("On Battery power, selecting Battery profile."); + log::info!("On Battery power, selecting Battery profile."); selected_profile_config = &config.battery; } } // Apply settings from selected_profile_config if let Some(governor) = &selected_profile_config.governor { - info!("Setting governor to '{governor}'"); + log::info!("Setting governor to '{governor}'"); // Let set_governor handle the validation if let Err(e) = cpu::set_governor(governor, None) { // If the governor is not available, log a warning if matches!(e, ControlError::InvalidGovernor(_)) || matches!(e, ControlError::NotSupported(_)) { - warn!( + log::warn!( "Configured governor '{governor}' is not available on this system. Skipping." ); } else { @@ -220,14 +219,14 @@ pub fn determine_and_apply_settings( } if let Some(turbo_setting) = selected_profile_config.turbo { - info!("Setting turbo to '{turbo_setting:?}'"); + log::info!("Setting turbo to '{turbo_setting:?}'"); match turbo_setting { TurboSetting::Auto => { if selected_profile_config.enable_auto_turbo { - debug!("Managing turbo in auto mode based on system conditions"); + log::debug!("Managing turbo in auto mode based on system conditions"); manage_auto_turbo(report, selected_profile_config, on_ac_power)?; } else { - debug!( + log::debug!( "Superfreq's dynamic turbo management is disabled by configuration. Ensuring system uses its default behavior for automatic turbo control." ); // Make sure the system is set to its default automatic turbo mode. @@ -255,13 +254,13 @@ pub fn determine_and_apply_settings( if let Some(min_freq) = selected_profile_config.min_freq_mhz { try_apply_feature("min frequency", &format!("{min_freq} MHz"), || { - cpu::set_min_frequency(min_freq, None) + cpu::set_frequency_minimum(min_freq, None) })?; } if let Some(max_freq) = selected_profile_config.max_freq_mhz { try_apply_feature("max frequency", &format!("{max_freq} MHz"), || { - cpu::set_max_frequency(max_freq, None) + cpu::set_frequency_maximum(max_freq, None) })?; } @@ -277,19 +276,19 @@ pub fn determine_and_apply_settings( let stop_threshold = thresholds.stop; if start_threshold < stop_threshold && stop_threshold <= 100 { - info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); + log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { - Ok(()) => debug!("Battery charge thresholds set successfully"), - Err(e) => warn!("Failed to set battery charge thresholds: {e}"), + Ok(()) => log::debug!("Battery charge thresholds set successfully"), + Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), } } else { - warn!( + log::warn!( "Invalid battery threshold values: start={start_threshold}, stop={stop_threshold}" ); } } - debug!("Profile settings applied successfully."); + log::debug!("Profile settings applied successfully."); Ok(()) } @@ -346,27 +345,30 @@ fn manage_auto_turbo( let enable_turbo = match (cpu_temp, avg_cpu_usage, previous_turbo_enabled) { // If temperature is too high, disable turbo regardless of load (Some(temp), _, _) if temp >= turbo_settings.temp_threshold_high => { - info!( + log::info!( "Auto Turbo: Disabled due to high temperature ({:.1}°C >= {:.1}°C)", - temp, turbo_settings.temp_threshold_high + temp, + turbo_settings.temp_threshold_high ); false } // If load is high enough, enable turbo (unless temp already caused it to disable) (_, Some(usage), _) if usage >= turbo_settings.load_threshold_high => { - info!( + log::info!( "Auto Turbo: Enabled due to high CPU load ({:.1}% >= {:.1}%)", - usage, turbo_settings.load_threshold_high + usage, + turbo_settings.load_threshold_high ); true } // If load is low, disable turbo (_, Some(usage), _) if usage <= turbo_settings.load_threshold_low => { - info!( + log::info!( "Auto Turbo: Disabled due to low CPU load ({:.1}% <= {:.1}%)", - usage, turbo_settings.load_threshold_low + usage, + turbo_settings.load_threshold_low ); false } @@ -376,7 +378,7 @@ fn manage_auto_turbo( if usage > turbo_settings.load_threshold_low && usage < turbo_settings.load_threshold_high => { - info!( + log::info!( "Auto Turbo: Maintaining previous state ({}) due to intermediate load ({:.1}%)", if prev_state { "enabled" } else { "disabled" }, usage @@ -386,7 +388,7 @@ fn manage_auto_turbo( // When CPU load data is present but temperature is missing, use the same hysteresis logic (None, Some(usage), prev_state) => { - info!( + log::info!( "Auto Turbo: Maintaining previous state ({}) due to missing temperature data (load: {:.1}%)", if prev_state { "enabled" } else { "disabled" }, usage @@ -396,7 +398,7 @@ fn manage_auto_turbo( // When all metrics are missing, maintain the previous state (None, None, prev_state) => { - info!( + log::info!( "Auto Turbo: Maintaining previous state ({}) due to missing all CPU metrics", if prev_state { "enabled" } else { "disabled" } ); @@ -405,7 +407,7 @@ fn manage_auto_turbo( // Any other cases with partial metrics, maintain previous state for stability (_, _, prev_state) => { - info!( + log::info!( "Auto Turbo: Maintaining previous state ({}) due to incomplete CPU metrics", if prev_state { "enabled" } else { "disabled" } ); @@ -429,7 +431,7 @@ fn manage_auto_turbo( TurboSetting::Never }; - info!( + log::info!( "Auto Turbo: Applying turbo change from {} to {}", if previous_turbo_enabled { "enabled" @@ -441,7 +443,7 @@ fn manage_auto_turbo( match cpu::set_turbo(turbo_setting) { Ok(()) => { - debug!( + log::debug!( "Auto Turbo: Successfully set turbo to {}", if enable_turbo { "enabled" } else { "disabled" } ); @@ -450,7 +452,7 @@ fn manage_auto_turbo( Err(e) => Err(EngineError::ControlError(e)), } } else { - debug!( + log::debug!( "Auto Turbo: Maintaining turbo state ({}) - no change needed", if enable_turbo { "enabled" } else { "disabled" } ); diff --git a/src/main.rs b/src/main.rs index edf762f..333cf64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,469 +8,478 @@ mod engine; mod monitor; mod util; -use crate::config::AppConfig; -use crate::core::{GovernorOverrideMode, TurboSetting}; -use crate::util::error::{AppError, ControlError}; -use clap::{Parser, value_parser}; -use env_logger::Builder; -use log::{debug, error, info}; -use std::error::Error; -use std::sync::Once; +use anyhow::{Context, anyhow, bail}; +use clap::Parser as _; +use std::fmt::Write as _; +use std::io::Write as _; +use std::{io, process}; +use yansi::Paint as _; -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] +#[derive(clap::Parser, Debug)] +#[clap(author, version, about)] struct Cli { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, + #[clap(subcommand)] - command: Option, + command: Command, } -#[derive(Parser, Debug)] -enum Commands { - /// Display current system information +#[derive(clap::Parser, Debug)] +enum Command { + /// Display information. Info, - /// Run as a daemon in the background - Daemon { - #[clap(long)] - verbose: bool, - }, - /// Set CPU governor - SetGovernor { - governor: String, - #[clap(long)] - core_id: Option, - }, - /// Force a specific governor mode persistently - ForceGovernor { - /// Mode to force: performance, powersave, or reset - #[clap(value_enum)] - mode: GovernorOverrideMode, - }, - /// Set turbo boost behavior - SetTurbo { - #[clap(value_enum)] - setting: TurboSetting, - }, - /// Display comprehensive debug information - Debug, - /// Set Energy Performance Preference (EPP) - SetEpp { - epp: String, - #[clap(long)] - core_id: Option, - }, - /// Set Energy Performance Bias (EPB) - SetEpb { - epb: String, // Typically 0-15 - #[clap(long)] - core_id: Option, - }, - /// Set minimum CPU frequency - SetMinFreq { - freq_mhz: u32, - #[clap(long)] - core_id: Option, - }, - /// Set maximum CPU frequency - SetMaxFreq { - freq_mhz: u32, - #[clap(long)] - core_id: Option, - }, - /// Set ACPI platform profile - SetPlatformProfile { profile: String }, - /// Set battery charge thresholds to extend battery lifespan - SetBatteryThresholds { - /// Percentage at which charging starts (when below this value) - #[clap(value_parser = value_parser!(u8).range(0..=99))] - start_threshold: u8, - /// Percentage at which charging stops (when it reaches this value) - #[clap(value_parser = value_parser!(u8).range(1..=100))] - stop_threshold: u8, + + /// Start the daemon. + Start, + + /// Modify attributes. + Set { + /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. + #[arg(short = 'c', long = "for")] + for_: Option>, + + /// Set the CPU governor. + #[arg(long)] + governor: Option, // TODO: Validate with clap for available governors. + + /// Set the CPU governor persistently. + #[arg(long, conflicts_with = "governor")] + governor_persist: Option, // TODO: Validate with clap for available governors. + + /// Set CPU Energy Performance Preference (EPP). Short form: --epp. + #[arg(long, alias = "epp")] + energy_performance_preference: Option, + + /// Set CPU Energy Performance Bias (EPB). Short form: --epb. + #[arg(long, alias = "epb")] + energy_performance_bias: Option, + + /// Set minimum CPU frequency in MHz. Short form: --freq-min. + #[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))] + frequency_mhz_minimum: Option, + + /// Set maximum CPU frequency in MHz. Short form: --freq-max. + #[arg(short = 'F', long, alias = "freq-max", value_parser = clap::value_parser!(u64).range(1..=10_000))] + frequency_mhz_maximum: Option, + + /// Set turbo boost behaviour. Has to be for all CPUs. + #[arg(long, conflicts_with = "for_")] + turbo: Option, + + /// Set ACPI platform profile. Has to be for all CPUs. + #[arg(long, alias = "profile", conflicts_with = "for_")] + platform_profile: Option, + + /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. + #[arg(short = 'p', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")] + charge_threshold_start: Option, + + /// Set the percentage where charging will stop. Short form: --charge-end. + #[arg(short = 'P', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100), conflicts_with = "for_")] + charge_threshold_end: Option, }, } -fn main() -> Result<(), AppError> { - // Initialize logger once for the entire application - init_logger(); - +fn real_main() -> anyhow::Result<()> { let cli = Cli::parse(); - // Load configuration first, as it might be needed by the monitor module - // E.g., for ignored power supplies - let config = match config::load_config() { - Ok(cfg) => cfg, - Err(e) => { - error!("Error loading configuration: {e}. Using default values."); - // Proceed with default config if loading fails - AppConfig::default() + env_logger::Builder::new() + .filter_level(cli.verbosity.log_level_filter()) + .format_timestamp(None) + .format_module_path(false) + .init(); + + let config = config::load_config().context("failed to load config")?; + + match cli.command { + Command::Info => todo!(), + + Command::Start => { + daemon::run_daemon(config)?; + Ok(()) } - }; - let command_result: Result<(), AppError> = match cli.command { - // TODO: This will be moved to a different module in the future. - Some(Commands::Info) => match monitor::collect_system_report(&config) { - Ok(report) => { - // Format section headers with proper centering - let format_section = |title: &str| { - let title_len = title.len(); - let total_width = title_len + 8; // 8 is for padding (4 on each side) - let separator = "═".repeat(total_width); + Command::Set { + for_, + governor, + governor_persist, + energy_performance_preference, + energy_performance_bias, + frequency_mhz_minimum, + frequency_mhz_maximum, + turbo, + platform_profile, + charge_threshold_start, + charge_threshold_end, + } => { + let cpus = match for_ { + Some(cpus) => cpus, + None => cpu::get_real_cpus()?, + }; - println!("\n╔{separator}╗"); + for cpu in cpus { + if let Some(governor) = governor.as_ref() { + cpu::set_governor(governor, cpu)?; + } - // Calculate centering - println!("║ {title} ║"); + if let Some(epp) = energy_performance_preference.as_ref() { + cpu::set_epp(epp, cpu)?; + } - println!("╚{separator}╝"); - }; + if let Some(epb) = energy_performance_bias.as_ref() { + cpu::set_epb(epb, cpu)?; + } - format_section("System Information"); - println!("CPU Model: {}", report.system_info.cpu_model); - println!("Architecture: {}", report.system_info.architecture); - println!( - "Linux Distribution: {}", - report.system_info.linux_distribution - ); + if let Some(mhz_minimum) = frequency_mhz_minimum { + cpu::set_frequency_minimum(mhz_minimum, cpu)?; + } - // Format timestamp in a readable way - println!("Current Time: {}", jiff::Timestamp::now()); + if let Some(mhz_maximum) = frequency_mhz_maximum { + cpu::set_frequency_maximum(mhz_maximum, cpu)?; + } + } - format_section("CPU Global Info"); - println!( - "Current Governor: {}", - report - .cpu_global - .current_governor - .as_deref() - .unwrap_or("N/A") - ); - println!( - "Available Governors: {}", // 21 length baseline - report.cpu_global.available_governors.join(", ") - ); - println!( - "Turbo Status: {}", - match report.cpu_global.turbo_status { - Some(true) => "Enabled", - Some(false) => "Disabled", - None => "Unknown", - } - ); + if let Some(turbo) = turbo { + cpu::set_turbo(turbo)?; + } - println!( - "EPP: {}", - report.cpu_global.epp.as_deref().unwrap_or("N/A") - ); - println!( - "EPB: {}", - report.cpu_global.epb.as_deref().unwrap_or("N/A") - ); - println!( - "Platform Profile: {}", - report - .cpu_global - .platform_profile - .as_deref() - .unwrap_or("N/A") - ); - println!( - "CPU Temperature: {}", - report.cpu_global.average_temperature_celsius.map_or_else( - || "N/A (No sensor detected)".to_string(), - |t| format!("{t:.1}°C") - ) - ); + if let Some(platform_profile) = platform_profile.as_ref() { + cpu::set_platform_profile(platform_profile)?; + } - format_section("CPU Core Info"); + // TODO: This is like this because [`cpu`] doesn't expose + // a way of setting them individually. Will clean this up + // after that is cleaned. + if charge_threshold_start.is_some() || charge_threshold_end.is_some() { + let charge_threshold_start = charge_threshold_start.ok_or_else(|| { + anyhow!("both charge thresholds should be given at the same time") + })?; + let charge_threshold_end = charge_threshold_end.ok_or_else(|| { + anyhow!("both charge thresholds should be given at the same time") + })?; - // Get max core ID length for padding - let max_core_id_len = report - .cpu_cores - .last() - .map_or(1, |core| core.core_id.to_string().len()); - - // Table headers - println!( - " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}", - "Core", - "Current", - "Min", - "Max", - "Usage", - "Temp", - width = max_core_id_len + 4 - ); - println!( - " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}", - "", - "", - "", - "", - "", - "", - width = max_core_id_len + 4 - ); - - for core_info in &report.cpu_cores { - // Format frequencies: if current > max, show in a special way - let current_freq = match core_info.current_frequency_mhz { - Some(freq) => { - let max_freq = core_info.max_frequency_mhz.unwrap_or(0); - if freq > max_freq && max_freq > 0 { - // Special format for boosted frequencies - format!("{freq}*") - } else { - format!("{freq}") - } - } - None => "N/A".to_string(), - }; - - // CPU core display - println!( - " Core {:10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}", - core_info.core_id, - format!("{} MHz", current_freq), - format!( - "{} MHz", - core_info - .min_frequency_mhz - .map_or_else(|| "N/A".to_string(), |f| f.to_string()) - ), - format!( - "{} MHz", - core_info - .max_frequency_mhz - .map_or_else(|| "N/A".to_string(), |f| f.to_string()) - ), - format!( - "{}%", - core_info - .usage_percent - .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) - ), - format!( - "{}°C", - core_info - .temperature_celsius - .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) - ), - width = max_core_id_len + if charge_threshold_start >= charge_threshold_end { + bail!( + "charge start threshold (given as {charge_threshold_start}) must be less than stop threshold (given as {charge_threshold_end})" ); } - // Only display battery info for systems that have real batteries - // Skip this section entirely on desktop systems - if !report.batteries.is_empty() { - let has_real_batteries = report.batteries.iter().any(|b| { - // Check if any battery has actual battery data - // (as opposed to peripherals like wireless mice) - b.capacity_percent.is_some() || b.power_rate_watts.is_some() - }); - - if has_real_batteries { - format_section("Battery Info"); - for battery_info in &report.batteries { - // Check if this appears to be a real system battery - if battery_info.capacity_percent.is_some() - || battery_info.power_rate_watts.is_some() - { - let power_status = if battery_info.ac_connected { - "Connected to AC" - } else { - "Running on Battery" - }; - - println!("Battery {}:", battery_info.name); - println!(" Power Status: {power_status}"); - println!( - " State: {}", - battery_info.charging_state.as_deref().unwrap_or("Unknown") - ); - - if let Some(capacity) = battery_info.capacity_percent { - println!(" Capacity: {capacity}%"); - } - - if let Some(power) = battery_info.power_rate_watts { - let direction = if power >= 0.0 { - "charging" - } else { - "discharging" - }; - println!( - " Power Rate: {:.2} W ({})", - power.abs(), - direction - ); - } - - // Display charge thresholds if available - if battery_info.charge_start_threshold.is_some() - || battery_info.charge_stop_threshold.is_some() - { - println!( - " Charge Thresholds: {}-{}", - battery_info - .charge_start_threshold - .map_or_else(|| "N/A".to_string(), |t| t.to_string()), - battery_info - .charge_stop_threshold - .map_or_else(|| "N/A".to_string(), |t| t.to_string()) - ); - } - } - } - } - } - - format_section("System Load"); - println!( - "Load Average (1m): {:.2}", - report.system_load.load_avg_1min - ); - println!( - "Load Average (5m): {:.2}", - report.system_load.load_avg_5min - ); - println!( - "Load Average (15m): {:.2}", - report.system_load.load_avg_15min - ); - Ok(()) + battery::set_battery_charge_thresholds( + charge_threshold_start, + charge_threshold_end, + )?; } - Err(e) => Err(AppError::Monitor(e)), - }, - Some(Commands::SetGovernor { governor, core_id }) => { - cpu::set_governor(&governor, core_id).map_err(AppError::Control) - } - Some(Commands::ForceGovernor { mode }) => { - cpu::force_governor(mode).map_err(AppError::Control) - } - Some(Commands::SetTurbo { setting }) => cpu::set_turbo(setting).map_err(AppError::Control), - Some(Commands::SetEpp { epp, core_id }) => { - cpu::set_epp(&epp, core_id).map_err(AppError::Control) - } - Some(Commands::SetEpb { epb, core_id }) => { - cpu::set_epb(&epb, core_id).map_err(AppError::Control) - } - Some(Commands::SetMinFreq { freq_mhz, core_id }) => { - // Basic validation for reasonable CPU frequency values - validate_freq(freq_mhz, "Minimum")?; - cpu::set_min_frequency(freq_mhz, core_id).map_err(AppError::Control) - } - Some(Commands::SetMaxFreq { freq_mhz, core_id }) => { - // Basic validation for reasonable CPU frequency values - validate_freq(freq_mhz, "Maximum")?; - cpu::set_max_frequency(freq_mhz, core_id).map_err(AppError::Control) - } - Some(Commands::SetPlatformProfile { profile }) => { - // Get available platform profiles and validate early if possible - match cpu::get_platform_profiles() { - Ok(available_profiles) => { - if available_profiles.contains(&profile) { - info!("Setting platform profile to '{profile}'"); - cpu::set_platform_profile(&profile).map_err(AppError::Control) - } else { - error!( - "Invalid platform profile: '{}'. Available profiles: {}", - profile, - available_profiles.join(", ") - ); - Err(AppError::Generic(format!( - "Invalid platform profile: '{}'. Available profiles: {}", - profile, - available_profiles.join(", ") - ))) - } - } - Err(_e) => { - // If we can't get profiles (e.g., feature not supported), pass through to the function - cpu::set_platform_profile(&profile).map_err(AppError::Control) - } - } - } - Some(Commands::SetBatteryThresholds { - start_threshold, - stop_threshold, - }) => { - // We only need to check if start < stop since the range validation is handled by Clap - if start_threshold >= stop_threshold { - error!( - "Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})" - ); - Err(AppError::Generic(format!( - "Start threshold ({start_threshold}) must be less than stop threshold ({stop_threshold})" - ))) - } else { - info!( - "Setting battery thresholds: start at {start_threshold}%, stop at {stop_threshold}%" - ); - battery::set_battery_charge_thresholds(start_threshold, stop_threshold) - .map_err(AppError::Control) - } - } - Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), - Some(Commands::Debug) => cli::debug::run_debug(&config), - None => { - info!("Welcome to superfreq! Use --help for commands."); - debug!("Current effective configuration: {config:?}"); + Ok(()) } + } + + // TODO: This will be moved to a different module in the future. + // Some(Command::Info) => match monitor::collect_system_report(&config) { + // Ok(report) => { + // // Format section headers with proper centering + // let format_section = |title: &str| { + // let title_len = title.len(); + // let total_width = title_len + 8; // 8 is for padding (4 on each side) + // let separator = "═".repeat(total_width); + + // println!("\n╔{separator}╗"); + + // // Calculate centering + // println!("║ {title} ║"); + + // println!("╚{separator}╝"); + // }; + + // format_section("System Information"); + // println!("CPU Model: {}", report.system_info.cpu_model); + // println!("Architecture: {}", report.system_info.architecture); + // println!( + // "Linux Distribution: {}", + // report.system_info.linux_distribution + // ); + + // // Format timestamp in a readable way + // println!("Current Time: {}", jiff::Timestamp::now()); + + // format_section("CPU Global Info"); + // println!( + // "Current Governor: {}", + // report + // .cpu_global + // .current_governor + // .as_deref() + // .unwrap_or("N/A") + // ); + // println!( + // "Available Governors: {}", // 21 length baseline + // report.cpu_global.available_governors.join(", ") + // ); + // println!( + // "Turbo Status: {}", + // match report.cpu_global.turbo_status { + // Some(true) => "Enabled", + // Some(false) => "Disabled", + // None => "Unknown", + // } + // ); + + // println!( + // "EPP: {}", + // report.cpu_global.epp.as_deref().unwrap_or("N/A") + // ); + // println!( + // "EPB: {}", + // report.cpu_global.epb.as_deref().unwrap_or("N/A") + // ); + // println!( + // "Platform Profile: {}", + // report + // .cpu_global + // .platform_profile + // .as_deref() + // .unwrap_or("N/A") + // ); + // println!( + // "CPU Temperature: {}", + // report.cpu_global.average_temperature_celsius.map_or_else( + // || "N/A (No sensor detected)".to_string(), + // |t| format!("{t:.1}°C") + // ) + // ); + + // format_section("CPU Core Info"); + + // // Get max core ID length for padding + // let max_core_id_len = report + // .cpu_cores + // .last() + // .map_or(1, |core| core.core_id.to_string().len()); + + // // Table headers + // println!( + // " {:>width$} │ {:^10} │ {:^10} │ {:^10} │ {:^7} │ {:^9}", + // "Core", + // "Current", + // "Min", + // "Max", + // "Usage", + // "Temp", + // width = max_core_id_len + 4 + // ); + // println!( + // " {:─>width$}──┼─{:─^10}─┼─{:─^10}─┼─{:─^10}─┼─{:─^7}─┼─{:─^9}", + // "", + // "", + // "", + // "", + // "", + // "", + // width = max_core_id_len + 4 + // ); + + // for core_info in &report.cpu_cores { + // // Format frequencies: if current > max, show in a special way + // let current_freq = match core_info.current_frequency_mhz { + // Some(freq) => { + // let max_freq = core_info.max_frequency_mhz.unwrap_or(0); + // if freq > max_freq && max_freq > 0 { + // // Special format for boosted frequencies + // format!("{freq}*") + // } else { + // format!("{freq}") + // } + // } + // None => "N/A".to_string(), + // }; + + // // CPU core display + // println!( + // " Core {:10} │ {:>10} │ {:>10} │ {:>7} │ {:>9}", + // core_info.core_id, + // format!("{} MHz", current_freq), + // format!( + // "{} MHz", + // core_info + // .min_frequency_mhz + // .map_or_else(|| "N/A".to_string(), |f| f.to_string()) + // ), + // format!( + // "{} MHz", + // core_info + // .max_frequency_mhz + // .map_or_else(|| "N/A".to_string(), |f| f.to_string()) + // ), + // format!( + // "{}%", + // core_info + // .usage_percent + // .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) + // ), + // format!( + // "{}°C", + // core_info + // .temperature_celsius + // .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) + // ), + // width = max_core_id_len + // ); + // } + + // // Only display battery info for systems that have real batteries + // // Skip this section entirely on desktop systems + // if !report.batteries.is_empty() { + // let has_real_batteries = report.batteries.iter().any(|b| { + // // Check if any battery has actual battery data + // // (as opposed to peripherals like wireless mice) + // b.capacity_percent.is_some() || b.power_rate_watts.is_some() + // }); + + // if has_real_batteries { + // format_section("Battery Info"); + // for battery_info in &report.batteries { + // // Check if this appears to be a real system battery + // if battery_info.capacity_percent.is_some() + // || battery_info.power_rate_watts.is_some() + // { + // let power_status = if battery_info.ac_connected { + // "Connected to AC" + // } else { + // "Running on Battery" + // }; + + // println!("Battery {}:", battery_info.name); + // println!(" Power Status: {power_status}"); + // println!( + // " State: {}", + // battery_info.charging_state.as_deref().unwrap_or("Unknown") + // ); + + // if let Some(capacity) = battery_info.capacity_percent { + // println!(" Capacity: {capacity}%"); + // } + + // if let Some(power) = battery_info.power_rate_watts { + // let direction = if power >= 0.0 { + // "charging" + // } else { + // "discharging" + // }; + // println!( + // " Power Rate: {:.2} W ({})", + // power.abs(), + // direction + // ); + // } + + // // Display charge thresholds if available + // if battery_info.charge_start_threshold.is_some() + // || battery_info.charge_stop_threshold.is_some() + // { + // println!( + // " Charge Thresholds: {}-{}", + // battery_info + // .charge_start_threshold + // .map_or_else(|| "N/A".to_string(), |t| t.to_string()), + // battery_info + // .charge_stop_threshold + // .map_or_else(|| "N/A".to_string(), |t| t.to_string()) + // ); + // } + // } + // } + // } + // } + + // format_section("System Load"); + // println!( + // "Load Average (1m): {:.2}", + // report.system_load.load_avg_1min + // ); + // println!( + // "Load Average (5m): {:.2}", + // report.system_load.load_avg_5min + // ); + // println!( + // "Load Average (15m): {:.2}", + // report.system_load.load_avg_15min + // ); + // Ok(()) + // } + // Err(e) => Err(AppError::Monitor(e)), + // }, + // Some(CliCommand::SetPlatformProfile { profile }) => { + // // Get available platform profiles and validate early if possible + // match cpu::get_platform_profiles() { + // Ok(available_profiles) => { + // if available_profiles.contains(&profile) { + // log::info!("Setting platform profile to '{profile}'"); + // cpu::set_platform_profile(&profile).map_err(AppError::Control) + // } else { + // log::error!( + // "Invalid platform profile: '{}'. Available profiles: {}", + // profile, + // available_profiles.join(", ") + // ); + // Err(AppError::Generic(format!( + // "Invalid platform profile: '{}'. Available profiles: {}", + // profile, + // available_profiles.join(", ") + // ))) + // } + // } + // Err(_e) => { + // // If we can't get profiles (e.g., feature not supported), pass through to the function + // cpu::set_platform_profile(&profile).map_err(AppError::Control) + // } + // } + // } +} + +fn main() { + let Err(error) = real_main() else { + return; }; - if let Err(e) = command_result { - error!("Error executing command: {e}"); - if let Some(source) = e.source() { - error!("Caused by: {source}"); - } + let mut err = io::stderr(); - // Check for permission denied errors - if let AppError::Control(control_error) = &e { - if matches!(control_error, ControlError::PermissionDenied(_)) { - error!( - "Hint: This operation may require administrator privileges (e.g., run with sudo)." - ); + let mut message = String::new(); + let mut chain = error.chain().rev().peekable(); + + while let Some(error) = chain.next() { + let _ = write!( + err, + "{header} ", + header = if chain.peek().is_none() { + "error:" + } else { + "cause:" } - } + .red() + .bold(), + ); - std::process::exit(1); + String::clear(&mut message); + let _ = write!(message, "{error}"); + + let mut chars = message.char_indices(); + + let _ = match (chars.next(), chars.next()) { + (Some((_, first)), Some((second_start, second))) if second.is_lowercase() => { + writeln!( + err, + "{first_lowercase}{rest}", + first_lowercase = first.to_lowercase(), + rest = &message[second_start..], + ) + } + + _ => { + writeln!(err, "{message}") + } + }; } - Ok(()) -} - -/// Initialize the logger for the entire application -static LOGGER_INIT: Once = Once::new(); -fn init_logger() { - LOGGER_INIT.call_once(|| { - // Set default log level based on environment or default to Info - let env_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); - - Builder::new() - .parse_filters(&env_log) - .format_timestamp(None) - .format_module_path(false) - .init(); - - debug!("Logger initialized with RUST_LOG={env_log}"); - }); -} - -/// Validate CPU frequency input values -fn validate_freq(freq_mhz: u32, label: &str) -> Result<(), AppError> { - if freq_mhz == 0 { - error!("{label} frequency cannot be zero"); - Err(AppError::Generic(format!( - "{label} frequency cannot be zero" - ))) - } else if freq_mhz > 10000 { - // Extremely high value unlikely to be valid - error!("{label} frequency ({freq_mhz} MHz) is unreasonably high"); - Err(AppError::Generic(format!( - "{label} frequency ({freq_mhz} MHz) is unreasonably high" - ))) - } else { - Ok(()) - } + process::exit(1); } diff --git a/src/monitor.rs b/src/monitor.rs index 80605ff..5724ae6 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,8 +1,7 @@ use crate::config::AppConfig; use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; -use crate::cpu::get_logical_core_count; +use crate::cpu::get_real_cpus; use crate::util::error::SysMonitorError; -use log::debug; use std::{ collections::HashMap, fs, @@ -364,7 +363,7 @@ 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() + let num_cores = get_real_cpus() .map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?; let mut core_infos = Vec::with_capacity(num_cores as usize); @@ -395,7 +394,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/"); if !cpufreq_base_path_buf.exists() { - let core_count = get_logical_core_count().unwrap_or_else(|e| { + let core_count = get_real_cpus().unwrap_or_else(|e| { eprintln!("Warning: {e}"); 0 }); @@ -551,7 +550,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { if ps_type == "Battery" { // Skip peripheral batteries that aren't real laptop batteries if is_peripheral_battery(&ps_path, &name) { - debug!("Skipping peripheral battery: {name}"); + log::debug!("Skipping peripheral battery: {name}"); continue; } @@ -598,7 +597,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { // If we found no batteries but have power supplies, we're likely on a desktop if batteries.is_empty() && overall_ac_connected { - debug!("No laptop batteries found, likely a desktop system"); + log::debug!("No laptop batteries found, likely a desktop system"); } Ok(batteries)