From 8f3abd1ca3a4bc80c8731d4883ab976c5f0024d5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:38:44 +0300 Subject: [PATCH 1/7] power_supply: don't ignore non-batteries --- src/main.rs | 68 +++++++++++++------ src/power_supply.rs | 155 +++++++++++++++++++++++++++++--------------- 2 files changed, 153 insertions(+), 70 deletions(-) diff --git a/src/main.rs b/src/main.rs index b86b414..bfca56f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,22 +33,22 @@ enum Command { /// Start the daemon. Start, - /// Modify attributes. - Set { + /// Modify CPU attributes. + CpuSet { /// 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)] + #[arg(short = 'g', long)] governor: Option, // TODO: Validate with clap for available governors. /// Set CPU Energy Performance Preference (EPP). Short form: --epp. - #[arg(long, alias = "epp")] + #[arg(short = 'p', long, alias = "epp")] energy_performance_preference: Option, /// Set CPU Energy Performance Bias (EPB). Short form: --epb. - #[arg(long, alias = "epb")] + #[arg(short = 'b', long, alias = "epb")] energy_performance_bias: Option, /// Set minimum CPU frequency in MHz. Short form: --freq-min. @@ -60,20 +60,27 @@ enum Command { frequency_mhz_maximum: Option, /// Set turbo boost behaviour. Has to be for all CPUs. - #[arg(long, conflicts_with = "for_")] + #[arg(short = 't', 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, + /// Modify power supply attributes. + PowerSet { + /// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. + #[arg(short = 'p', long = "for")] + for_: 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_")] + #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] 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_")] + #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] charge_threshold_end: Option, + + /// Set ACPI platform profile. Has to be for all power supplies. + #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] + platform_profile: Option, }, } @@ -96,7 +103,7 @@ fn real_main() -> anyhow::Result<()> { Ok(()) } - Command::Set { + Command::CpuSet { for_, governor, energy_performance_preference, @@ -104,9 +111,6 @@ fn real_main() -> anyhow::Result<()> { frequency_mhz_minimum, frequency_mhz_maximum, turbo, - platform_profile, - charge_threshold_start, - charge_threshold_end, } => { let cpus = match for_ { Some(cpus) => cpus, @@ -139,11 +143,33 @@ fn real_main() -> anyhow::Result<()> { cpu::set_turbo(turbo)?; } - if let Some(platform_profile) = platform_profile.as_ref() { - cpu::set_platform_profile(platform_profile)?; - } + Ok(()) + } - for power_supply in power_supply::get_power_supplies()? { + Command::PowerSet { + for_, + charge_threshold_start, + charge_threshold_end, + platform_profile, + } => { + let power_supplies = match for_ { + Some(names) => { + let power_supplies = Vec::with_capacity(names.len()); + + for name in names { + power_supplies.push(power_supply::get_power_supply(&name)?); + } + + power_supplies + } + + None => power_supply::get_power_supplies()? + .into_iter() + .filter(|power_supply| power_supply.threshold_config.is_some()) + .collect(), + }; + + for power_supply in power_supplies { if let Some(threshold_start) = charge_threshold_start { power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; } @@ -153,6 +179,10 @@ fn real_main() -> anyhow::Result<()> { } } + if let Some(platform_profile) = platform_profile.as_ref() { + cpu::set_platform_profile(platform_profile)?; + } + Ok(()) } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 7320f18..bd1cd74 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -2,38 +2,39 @@ use anyhow::Context; use std::{ fmt, fs, + os::macos::fs::MetadataExt, path::{Path, PathBuf}, }; /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PowerSupplyConfig { +pub struct PowerSupplyThresholdConfig { pub manufacturer: &'static str, pub path_start: &'static str, pub path_end: &'static str, } -/// Charge threshold configs. -const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ - PowerSupplyConfig { +/// Power supply threshold configs. +const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ + PowerSupplyThresholdConfig { manufacturer: "Standard", path_start: "charge_control_start_threshold", path_end: "charge_control_end_threshold", }, - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "ASUS", path_start: "charge_control_start_percentage", path_end: "charge_control_end_percentage", }, // Combine Huawei and ThinkPad since they use identical paths. - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "ThinkPad/Huawei", path_start: "charge_start_threshold", path_end: "charge_stop_threshold", }, // Framework laptop support. - PowerSupplyConfig { + PowerSupplyThresholdConfig { manufacturer: "Framework", path_start: "charge_behaviour_start_threshold", path_end: "charge_behaviour_end_threshold", @@ -44,27 +45,34 @@ const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ pub struct PowerSupply { pub name: String, pub path: PathBuf, - pub config: PowerSupplyConfig, + pub threshold_config: Option, } impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "power suppply '{name}' from manufacturer '{manufacturer}'", - name = &self.name, - manufacturer = &self.config.manufacturer, - ) + write!(f, "power supply '{name}'", name = &self.name)?; + + if let Some(config) = self.threshold_config.as_ref() { + write!( + f, + " from manufacturer '{manufacturer}'", + manufacturer = config.manufacturer, + )?; + } + + Ok(()) } } impl PowerSupply { - pub fn charge_threshold_path_start(&self) -> PathBuf { - self.path.join(self.config.path_start) + pub fn charge_threshold_path_start(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_start)) } - pub fn charge_threshold_path_end(&self) -> PathBuf { - self.path.join(self.config.path_end) + pub fn charge_threshold_path_end(&self) -> Option { + self.threshold_config + .map(|config| self.path.join(config.path_end)) } } @@ -80,7 +88,7 @@ fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { }) } -fn is_power_supply(path: &Path) -> anyhow::Result { +fn is_battery(path: &Path) -> anyhow::Result { let type_path = path.join("type"); let type_ = fs::read_to_string(&type_path) @@ -89,13 +97,46 @@ fn is_power_supply(path: &Path) -> anyhow::Result { Ok(type_ == "Battery") } -/// Get all batteries in the system that support threshold control. -pub fn get_power_supplies() -> anyhow::Result> { - const PATH: &str = "/sys/class/power_supply"; +const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; +/// Get power supply. +pub fn get_power_supply(name: &str) -> anyhow::Result { + let entry_path = Path::new(POWER_SUPPLY_PATH).join(name); + + let threshold_config = is_battery(&entry_path) + .with_context(|| { + format!( + "failed to determine what type of power supply '{path}' is", + path = entry_path.display(), + ) + })? + .then(|| { + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if entry_path.join(config.path_start).exists() + && entry_path.join(config.path_end).exists() + { + return Some(*config); + } + } + + None + }) + .flatten(); + + Ok(PowerSupply { + name: name.to_owned(), + path: entry_path, + threshold_config, + }) +} + +/// Get all power supplies. +pub fn get_power_supplies() -> anyhow::Result> { let mut power_supplies = Vec::new(); - 'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{PATH}'"))? { + for entry in fs::read_dir(POWER_SUPPLY_PATH) + .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + { let entry = match entry { Ok(entry) => entry, @@ -107,38 +148,40 @@ pub fn get_power_supplies() -> anyhow::Result> { let entry_path = entry.path(); - if !is_power_supply(&entry_path).with_context(|| { + let mut power_supply_config = None; + + if is_battery(&entry_path).with_context(|| { format!( - "failed to determine whether if '{path}' is a power supply", + "failed to determine what type of power supply '{path}' is", path = entry_path.display(), ) })? { - continue; - } - - for config in POWER_SUPPLY_CONFIGS { - if entry_path.join(config.path_start).exists() - && entry_path.join(config.path_end).exists() - { - power_supplies.push(PowerSupply { - name: entry_path - .file_name() - .with_context(|| { - format!( - "failed to get file name of '{path}'", - path = entry_path.display(), - ) - })? - .to_string_lossy() - .to_string(), - - path: entry_path, - - config: *config, - }); - continue 'entries; + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if entry_path.join(config.path_start).exists() + && entry_path.join(config.path_end).exists() + { + power_supply_config = Some(*config); + break; + } } } + + power_supplies.push(PowerSupply { + name: entry_path + .file_name() + .with_context(|| { + format!( + "failed to get file name of '{path}'", + path = entry_path.display(), + ) + })? + .to_string_lossy() + .to_string(), + + path: entry_path, + + threshold_config: power_supply_config, + }); } Ok(power_supplies) @@ -149,7 +192,12 @@ pub fn set_charge_threshold_start( charge_threshold_start: u8, ) -> anyhow::Result<()> { write( - &power_supply.charge_threshold_path_start(), + &power_supply.charge_threshold_path_start().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = power_supply.name, + ) + })?, &charge_threshold_start.to_string(), ) .with_context(|| format!("failed to set charge threshold start for {power_supply}"))?; @@ -164,7 +212,12 @@ pub fn set_charge_threshold_end( charge_threshold_end: u8, ) -> anyhow::Result<()> { write( - &power_supply.charge_threshold_path_end(), + &power_supply.charge_threshold_path_end().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = power_supply.name, + ) + })?, &charge_threshold_end.to_string(), ) .with_context(|| format!("failed to set charge threshold end for {power_supply}"))?; From bc343eefd9cb40245e3e5cfe631a875c70c14fdb Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 16:17:54 +0300 Subject: [PATCH 2/7] power_supply&cpu: use objects --- src/cpu.rs | 645 +++++++++++++++++++------------------------- src/main.rs | 36 ++- src/power_supply.rs | 317 ++++++++++++---------- 3 files changed, 473 insertions(+), 525 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 0f5f304..ff79ccd 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,19 +1,6 @@ use anyhow::{Context, bail}; -use derive_more::Display; -use serde::{Deserialize, Serialize}; -use std::{fs, io, path::Path, string::ToString}; - -// // 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", -// ]; +use std::{fs, path::Path, string::ToString}; fn exists(path: impl AsRef) -> bool { let path = path.as_ref(); @@ -41,394 +28,326 @@ fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { }) } -/// Get real, tunable CPUs. -pub fn get_real_cpus() -> anyhow::Result> { - const PATH: &str = "/sys/devices/system/cpu"; +pub struct Cpu { + pub number: u32, + pub has_cpufreq: bool, +} - let mut cpus = vec![]; - - 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; +impl Cpu { + pub fn new(number: u32) -> anyhow::Result { + let mut cpu = Self { + number, + has_cpufreq: false, }; + cpu.rescan()?; - let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { - continue; - }; + Ok(cpu) + } - // Has to match "cpu{N}". - let Ok(cpu) = cpu_prefix_removed.parse::() else { - continue; - }; + /// Get all CPUs. + pub fn all() -> anyhow::Result> { + const PATH: &str = "/sys/devices/system/cpu"; - // Has to match "cpu{N}/cpufreq". - if !entry.path().join("cpufreq").exists() { - continue; + let mut cpus = vec![]; + + 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; + }; + + let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else { + continue; + }; + + // Has to match "cpu{N}". + let Ok(number) = cpu_prefix_removed.parse::() else { + continue; + }; + + cpus.push(Self::new(number)?); } - cpus.push(cpu); + // Fall back if sysfs iteration above fails to find any cpufreq CPUs. + if cpus.is_empty() { + for number in 0..num_cpus::get() as u32 { + cpus.push(Self::new(number)?); + } + } + + Ok(cpus) } - // Fall back if sysfs iteration above fails to find any cpufreq CPUs. - if cpus.is_empty() { - cpus = (0..num_cpus::get() as u32).collect(); + /// Rescan CPU, tuning local copy of settings. + pub fn rescan(&mut self) -> anyhow::Result<()> { + let has_cpufreq = exists(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq", + number = self.number, + )); + + self.has_cpufreq = has_cpufreq; + + Ok(()) } - Ok(cpus) -} + pub fn get_available_governors(&self) -> Vec { + let Self { number, .. } = self; -/// Set the governor for a CPU. -pub fn set_governor(governor: &str, cpu: u32) -> anyhow::Result<()> { - let governors = get_available_governors_for(cpu); + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" + )) else { + return Vec::new(); + }; - if !governors - .iter() - .any(|avail_governor| avail_governor == governor) - { - bail!( - "governor '{governor}' is not available for CPU {cpu}. valid governors: {governors}", - governors = governors.join(", "), - ); + content + .split_whitespace() + .map(ToString::to_string) + .collect() } - 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(&self, governor: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; -/// 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(); - }; + let governors = self.get_available_governors(); - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} + if !governors + .iter() + .any(|avail_governor| avail_governor == governor) + { + bail!( + "governor '{governor}' is not available for CPU {number}. available governors: {governors}", + governors = governors.join(", "), + ); + } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, clap::ValueEnum)] -pub enum Turbo { - Always, - Never, -} - -pub fn set_turbo(setting: Turbo) -> anyhow::Result<()> { - let value_boost = match setting { - 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_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 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 write(intel_boost_path_negated, value_boost_negated).is_ok() { - return Ok(()); - } - 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(()); - } - - // 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, + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), + governor, ) - .is_ok() - }) { - return Ok(()); + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing governors" + ) + }) } - bail!("no supported CPU boost control mechanism found"); -} + pub fn get_available_epps(&self) -> Vec { + let Self { number, .. } = self; -pub fn set_epp(epp: &str, cpu: u32) -> anyhow::Result<()> { - // Validate the EPP value against available options - let epps = get_available_epps(cpu); + let Ok(content) = fs::read_to_string(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" + )) else { + return Vec::new(); + }; - 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(", "), - ); + content + .split_whitespace() + .map(ToString::to_string) + .collect() } - 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") - }) -} + pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; -/// 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(); - }; + let epps = self.get_available_epps(); - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} + if !epps.iter().any(|avail_epp| avail_epp == epp) { + bail!( + "EPP value '{epp}' is not availabile for CPU {number}. available EPP values: {epps}", + epps = epps.join(", "), + ); + } -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)?; + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), + epp, + ) + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing EPP" + ) + }) + } - 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") - }) -} + pub fn get_available_epbs(&self) -> &'static [&'static str] { + if !self.has_cpufreq { + return &[]; + } -fn validate_epb_value(epb: &str) -> anyhow::Result<()> { - // EPB can be a number from 0-15 or a recognized string. + &[ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "performance", + "balance-performance", + "balance_performance", // Alternative form with underscore. + "balance-power", + "balance_power", // Alternative form with underscore. + "power", + ] + } - const VALID_EPB_STRINGS: &[&str] = &[ - "performance", - "balance-performance", - "balance_performance", // Alternative form with underscore. - "balance-power", - "balance_power", // Alternative form with underscore. - "power", - ]; + pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { + let Self { number, .. } = self; - // Try parsing as a number first. - if let Ok(value) = epb.parse::() { - if value <= 15 { + let epbs = self.get_available_epbs(); + + if !epbs.contains(&epb) { + bail!( + "EPB value '{epb}' is not available for CPU {number}. available EPB values: {valid}", + valid = epbs.join(", "), + ); + } + + write( + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), + epb, + ) + .with_context(|| { + format!( + "this probably means that CPU {number} doesn't exist or doesn't support changing EPB" + ) + }) + } + + pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.validate_frequency_minimum(frequency_mhz)?; + + // 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{number}/cpufreq/scaling_min_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {number} doesn't exist or doesn't support changing minimum frequency") + }) + } + + fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Ok(minimum_frequency_khz) = read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/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 {number}", + minimum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.validate_frequency_maximum(frequency_mhz)?; + + // 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{number}/cpufreq/scaling_max_freq"), + &frequency_khz, + ) + .with_context(|| { + format!("this probably means that CPU {number} doesn't exist or doesn't support changing maximum frequency") + }) + } + + fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = self; + + let Ok(maximum_frequency_khz) = read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) else { + // Just let it pass if we can't find anything. + return Ok(()); + }; + + 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 {number}", + maximum_frequency_khz / 1000, + ); + } + + Ok(()) + } + + pub fn set_turbo(on: bool) -> anyhow::Result<()> { + let value_boost = match on { + true => "1", // boost = 1 means turbo is enabled. + false => "0", // boost = 0 means turbo is disabled. + }; + + let value_boost_negated = match on { + true => "0", // no_turbo = 0 means turbo is enabled. + false => "1", // no_turbo = 1 means turbo is disabled. + }; + + // AMD specific paths + 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 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 write(intel_boost_path_negated, value_boost_negated).is_ok() { + return Ok(()); + } + 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(()); } - bail!("EPB numeric value must be between 0 and 15, got {value}"); - } + // Also try per-core cpufreq boost for some AMD systems. + if Self::all()?.iter().any(|cpu| { + let Cpu { number, .. } = cpu; - // If not a number, check if it's a recognized string value. - if VALID_EPB_STRINGS.contains(&epb) { - return Ok(()); - } - - bail!( - "invalid EPB value: '{epb}'. must be a number between 0-15 inclusive or one of: {valid}", - valid = VALID_EPB_STRINGS.join(", "), - ); -} - -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(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(()); - }; - - 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(()) -} - -/// Sets the platform profile. -/// This changes the system performance, temperature, fan, and other hardware replated characteristics. -/// -/// Also see [`The Kernel docs`] for this. -/// -/// [`The Kernel docs`]: -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(", "), - ); - } - - write("/sys/firmware/acpi/platform_profile", profile) - .context("this probably means that your system does not support changing ACPI profiles") -} - -/// Get the list of available platform profiles. -pub fn get_platform_profiles() -> Vec { - let path = "/sys/firmware/acpi/platform_profile_choices"; - - let Ok(content) = fs::read_to_string(path) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() -} - -/// Path for storing the governor override state. -const GOVERNOR_OVERRIDE_PATH: &str = "/etc/xdg/superfreq/governor_override"; - -#[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(), + write( + &format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + value_boost, ) - })?; - } - - match mode { - GovernorOverride::Reset => { - // Remove the override file if it exists - 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}'" - ) - }); - } - } - - log::info!( - "governor override has been deleted. normal profile-based settings will be used" - ); + .is_ok() + }) { + return 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() -> 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}'") - }), + bail!("no supported CPU boost control mechanism found"); } } diff --git a/src/main.rs b/src/main.rs index bfca56f..f4a7120 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ enum Command { /// Set turbo boost behaviour. Has to be for all CPUs. #[arg(short = 't', long, conflicts_with = "for_")] - turbo: Option, + turbo: Option, }, /// Modify power supply attributes. @@ -113,34 +113,42 @@ fn real_main() -> anyhow::Result<()> { turbo, } => { let cpus = match for_ { - Some(cpus) => cpus, - None => cpu::get_real_cpus()?, + Some(numbers) => { + let mut cpus = Vec::with_capacity(numbers.len()); + + for number in numbers { + cpus.push(cpu::Cpu::new(number)?); + } + + cpus + } + None => cpu::Cpu::all()?, }; for cpu in cpus { if let Some(governor) = governor.as_ref() { - cpu::set_governor(governor, cpu)?; + cpu.set_governor(governor)?; } if let Some(epp) = energy_performance_preference.as_ref() { - cpu::set_epp(epp, cpu)?; + cpu.set_epp(epp)?; } if let Some(epb) = energy_performance_bias.as_ref() { - cpu::set_epb(epb, cpu)?; + cpu.set_epb(epb)?; } if let Some(mhz_minimum) = frequency_mhz_minimum { - cpu::set_frequency_minimum(mhz_minimum, cpu)?; + cpu.set_frequency_minimum(mhz_minimum)?; } if let Some(mhz_maximum) = frequency_mhz_maximum { - cpu::set_frequency_maximum(mhz_maximum, cpu)?; + cpu.set_frequency_maximum(mhz_maximum)?; } } if let Some(turbo) = turbo { - cpu::set_turbo(turbo)?; + cpu::Cpu::set_turbo(turbo)?; } Ok(()) @@ -157,13 +165,13 @@ fn real_main() -> anyhow::Result<()> { let power_supplies = Vec::with_capacity(names.len()); for name in names { - power_supplies.push(power_supply::get_power_supply(&name)?); + power_supplies.push(power_supply::PowerSupply::from_name(name)?); } power_supplies } - None => power_supply::get_power_supplies()? + None => power_supply::PowerSupply::all()? .into_iter() .filter(|power_supply| power_supply.threshold_config.is_some()) .collect(), @@ -171,16 +179,16 @@ fn real_main() -> anyhow::Result<()> { for power_supply in power_supplies { if let Some(threshold_start) = charge_threshold_start { - power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; + power_supply.set_charge_threshold_start(threshold_start)?; } if let Some(threshold_end) = charge_threshold_end { - power_supply::set_charge_threshold_end(&power_supply, threshold_end)?; + power_supply.set_charge_threshold_end(threshold_end)?; } } if let Some(platform_profile) = platform_profile.as_ref() { - cpu::set_platform_profile(platform_profile)?; + power_supply::PowerSupply::set_platform_profile(platform_profile); } Ok(()) diff --git a/src/power_supply.rs b/src/power_supply.rs index bd1cd74..92147da 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,11 +1,22 @@ -use anyhow::Context; +use anyhow::{Context, bail}; use std::{ fmt, fs, - os::macos::fs::MetadataExt, path::{Path, PathBuf}, }; +// TODO: Migrate to central utils file. Same exists in cpu.rs. +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(), + ) + }) +} + /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,7 +61,12 @@ pub struct PowerSupply { impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "power supply '{name}'", name = &self.name)?; + write!( + f, + "power supply '{name}' at '{path}'", + name = &self.name, + path = self.path.display(), + )?; if let Some(config) = self.threshold_config.as_ref() { write!( @@ -64,7 +80,93 @@ impl fmt::Display for PowerSupply { } } +const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; + impl PowerSupply { + pub fn from_name(name: String) -> anyhow::Result { + let mut power_supply = Self { + path: Path::new(POWER_SUPPLY_PATH).join(&name), + name, + threshold_config: None, + }; + + power_supply.rescan()?; + + Ok(power_supply) + } + + pub fn from_path(path: PathBuf) -> anyhow::Result { + let mut power_supply = PowerSupply { + name: path + .file_name() + .with_context(|| { + format!("failed to get file name of '{path}'", path = path.display(),) + })? + .to_string_lossy() + .to_string(), + + path, + + threshold_config: None, + }; + + power_supply.rescan()?; + + Ok(power_supply) + } + + pub fn all() -> anyhow::Result> { + let mut power_supplies = Vec::new(); + + for entry in fs::read_dir(POWER_SUPPLY_PATH) + .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + { + let entry = match entry { + Ok(entry) => entry, + + Err(error) => { + log::warn!("failed to read power supply entry: {error}"); + continue; + } + }; + + power_supplies.push(PowerSupply::from_path(entry.path())?); + } + + Ok(power_supplies) + } + + fn is_battery(&self) -> anyhow::Result { + let type_path = self.path.join("type"); + + let type_ = fs::read_to_string(&type_path) + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; + + Ok(type_ == "Battery") + } + + pub fn rescan(&mut self) -> anyhow::Result<()> { + let threshold_config = self + .is_battery() + .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? + .then(|| { + for config in POWER_SUPPLY_THRESHOLD_CONFIGS { + if self.path.join(config.path_start).exists() + && self.path.join(config.path_end).exists() + { + return Some(*config); + } + } + + None + }) + .flatten(); + + self.threshold_config = threshold_config; + + Ok(()) + } + pub fn charge_threshold_path_start(&self) -> Option { self.threshold_config .map(|config| self.path.join(config.path_start)) @@ -74,155 +176,74 @@ impl PowerSupply { self.threshold_config .map(|config| self.path.join(config.path_end)) } -} -// TODO: Migrate to central utils file. Same exists in cpu.rs. -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 set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { + write( + &self.charge_threshold_path_start().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &charge_threshold_start.to_string(), ) - }) -} + .with_context(|| format!("failed to set charge threshold start for {self}"))?; -fn is_battery(path: &Path) -> anyhow::Result { - let type_path = path.join("type"); + log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); - let type_ = fs::read_to_string(&type_path) - .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - - Ok(type_ == "Battery") -} - -const POWER_SUPPLY_PATH: &str = "/sys/class/power_supply"; - -/// Get power supply. -pub fn get_power_supply(name: &str) -> anyhow::Result { - let entry_path = Path::new(POWER_SUPPLY_PATH).join(name); - - let threshold_config = is_battery(&entry_path) - .with_context(|| { - format!( - "failed to determine what type of power supply '{path}' is", - path = entry_path.display(), - ) - })? - .then(|| { - for config in POWER_SUPPLY_THRESHOLD_CONFIGS { - if entry_path.join(config.path_start).exists() - && entry_path.join(config.path_end).exists() - { - return Some(*config); - } - } - - None - }) - .flatten(); - - Ok(PowerSupply { - name: name.to_owned(), - path: entry_path, - threshold_config, - }) -} - -/// Get all power supplies. -pub fn get_power_supplies() -> anyhow::Result> { - let mut power_supplies = Vec::new(); - - for entry in fs::read_dir(POWER_SUPPLY_PATH) - .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? - { - let entry = match entry { - Ok(entry) => entry, - - Err(error) => { - log::warn!("failed to read power supply entry: {error}"); - continue; - } - }; - - let entry_path = entry.path(); - - let mut power_supply_config = None; - - if is_battery(&entry_path).with_context(|| { - format!( - "failed to determine what type of power supply '{path}' is", - path = entry_path.display(), - ) - })? { - for config in POWER_SUPPLY_THRESHOLD_CONFIGS { - if entry_path.join(config.path_start).exists() - && entry_path.join(config.path_end).exists() - { - power_supply_config = Some(*config); - break; - } - } - } - - power_supplies.push(PowerSupply { - name: entry_path - .file_name() - .with_context(|| { - format!( - "failed to get file name of '{path}'", - path = entry_path.display(), - ) - })? - .to_string_lossy() - .to_string(), - - path: entry_path, - - threshold_config: power_supply_config, - }); + Ok(()) } - Ok(power_supplies) -} - -pub fn set_charge_threshold_start( - power_supply: &PowerSupply, - charge_threshold_start: u8, -) -> anyhow::Result<()> { - write( - &power_supply.charge_threshold_path_start().ok_or_else(|| { - anyhow::anyhow!( - "power supply '{name}' does not support changing charge threshold levels", - name = power_supply.name, - ) - })?, - &charge_threshold_start.to_string(), - ) - .with_context(|| format!("failed to set charge threshold start for {power_supply}"))?; - - log::info!("set battery threshold start for {power_supply} to {charge_threshold_start}%"); - - Ok(()) -} - -pub fn set_charge_threshold_end( - power_supply: &PowerSupply, - charge_threshold_end: u8, -) -> anyhow::Result<()> { - write( - &power_supply.charge_threshold_path_end().ok_or_else(|| { - anyhow::anyhow!( - "power supply '{name}' does not support changing charge threshold levels", - name = power_supply.name, - ) - })?, - &charge_threshold_end.to_string(), - ) - .with_context(|| format!("failed to set charge threshold end for {power_supply}"))?; - - log::info!("set battery threshold end for {power_supply} to {charge_threshold_end}%"); - - Ok(()) + pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { + write( + &self.charge_threshold_path_end().ok_or_else(|| { + anyhow::anyhow!( + "power supply '{name}' does not support changing charge threshold levels", + name = self.name, + ) + })?, + &charge_threshold_end.to_string(), + ) + .with_context(|| format!("failed to set charge threshold end for {self}"))?; + + log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); + + Ok(()) + } + + pub fn get_available_platform_profiles() -> Vec { + let path = "/sys/firmware/acpi/platform_profile_choices"; + + let Ok(content) = fs::read_to_string(path) else { + return Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + } + + /// Sets the platform profile. + /// This changes the system performance, temperature, fan, and other hardware replated characteristics. + /// + /// Also see [`The Kernel docs`] for this. + /// + /// [`The Kernel docs`]: + pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { + let profiles = Self::get_available_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(", "), + ); + } + + write("/sys/firmware/acpi/platform_profile", profile) + .context("this probably means that your system does not support changing ACPI profiles") + } } From d1247c157048916fcc7af4ea75a8e8a0e939d13b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:40:45 +0300 Subject: [PATCH 3/7] cpu: impl Display for Cpu --- src/cpu.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index ff79ccd..2d7a32d 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,6 +1,6 @@ use anyhow::{Context, bail}; -use std::{fs, path::Path, string::ToString}; +use std::{fmt, fs, path::Path, string::ToString}; fn exists(path: impl AsRef) -> bool { let path = path.as_ref(); @@ -28,11 +28,19 @@ fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { }) } +#[derive(Debug, Clone, Copy)] pub struct Cpu { pub number: u32, pub has_cpufreq: bool, } +impl fmt::Display for Cpu { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { number, .. } = self; + write!(f, "CPU {number}") + } +} + impl Cpu { pub fn new(number: u32) -> anyhow::Result { let mut cpu = Self { @@ -119,7 +127,7 @@ impl Cpu { .any(|avail_governor| avail_governor == governor) { bail!( - "governor '{governor}' is not available for CPU {number}. available governors: {governors}", + "governor '{governor}' is not available for {self}. available governors: {governors}", governors = governors.join(", "), ); } @@ -130,7 +138,7 @@ impl Cpu { ) .with_context(|| { format!( - "this probably means that CPU {number} doesn't exist or doesn't support changing governors" + "this probably means that {self} doesn't exist or doesn't support changing governors" ) }) } @@ -157,7 +165,7 @@ impl Cpu { if !epps.iter().any(|avail_epp| avail_epp == epp) { bail!( - "EPP value '{epp}' is not availabile for CPU {number}. available EPP values: {epps}", + "EPP value '{epp}' is not availabile for {self}. available EPP values: {epps}", epps = epps.join(", "), ); } @@ -167,9 +175,7 @@ impl Cpu { epp, ) .with_context(|| { - format!( - "this probably means that CPU {number} doesn't exist or doesn't support changing EPP" - ) + format!("this probably means that {self} doesn't exist or doesn't support changing EPP") }) } @@ -210,7 +216,7 @@ impl Cpu { if !epbs.contains(&epb) { bail!( - "EPB value '{epb}' is not available for CPU {number}. available EPB values: {valid}", + "EPB value '{epb}' is not available for {self}. available EPB values: {valid}", valid = epbs.join(", "), ); } @@ -220,9 +226,7 @@ impl Cpu { epb, ) .with_context(|| { - format!( - "this probably means that CPU {number} doesn't exist or doesn't support changing EPB" - ) + format!("this probably means that {self} doesn't exist or doesn't support changing EPB") }) } @@ -240,7 +244,7 @@ impl Cpu { &frequency_khz, ) .with_context(|| { - format!("this probably means that CPU {number} doesn't exist or doesn't support changing minimum frequency") + format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") }) } @@ -256,7 +260,7 @@ impl Cpu { 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 {number}", + "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}", minimum_frequency_khz / 1000, ); } @@ -278,7 +282,7 @@ impl Cpu { &frequency_khz, ) .with_context(|| { - format!("this probably means that CPU {number} doesn't exist or doesn't support changing maximum frequency") + format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") }) } @@ -294,7 +298,7 @@ impl Cpu { 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 {number}", + "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for {self}", maximum_frequency_khz / 1000, ); } From d61564d5f56047ff0347cb420c938435c2647f63 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:43:21 +0300 Subject: [PATCH 4/7] wip unsound broken malfunctioning changes to make it compile --- src/cli/debug.rs | 265 -------------------------------------------- src/cli/mod.rs | 1 - src/config/load.rs | 25 +++-- src/config/types.rs | 112 +++++++------------ src/daemon.rs | 37 +++---- src/engine.rs | 67 ++++------- src/main.rs | 4 +- src/monitor.rs | 24 ++-- src/util/error.rs | 80 ------------- src/util/mod.rs | 2 - src/util/sysfs.rs | 80 ------------- 11 files changed, 106 insertions(+), 591 deletions(-) delete mode 100644 src/cli/debug.rs delete mode 100644 src/cli/mod.rs delete mode 100644 src/util/error.rs delete mode 100644 src/util/mod.rs delete mode 100644 src/util/sysfs.rs diff --git a/src/cli/debug.rs b/src/cli/debug.rs deleted file mode 100644 index 17cec0c..0000000 --- a/src/cli/debug.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::config::AppConfig; -use crate::cpu; -use crate::monitor; -use crate::util::error::AppError; -use std::fs; -use std::process::{Command, Stdio}; -use std::time::Duration; - -/// Prints comprehensive debug information about the system -pub fn run_debug(config: &AppConfig) -> Result<(), AppError> { - println!("=== SUPERFREQ DEBUG INFORMATION ==="); - println!("Version: {}", env!("CARGO_PKG_VERSION")); - - // Current date and time - println!("Timestamp: {}", jiff::Timestamp::now()); - - // Kernel information - if let Ok(kernel_info) = get_kernel_info() { - println!("Kernel Version: {kernel_info}"); - } else { - println!("Kernel Version: Unable to determine"); - } - - // System uptime - if let Ok(uptime) = get_system_uptime() { - println!( - "System Uptime: {} hours, {} minutes", - uptime.as_secs() / 3600, - (uptime.as_secs() % 3600) / 60 - ); - } else { - println!("System Uptime: Unable to determine"); - } - - // Get system information - match monitor::collect_system_report(config) { - Ok(report) => { - println!("\n--- SYSTEM INFORMATION ---"); - println!("CPU Model: {}", report.system_info.cpu_model); - println!("Architecture: {}", report.system_info.architecture); - println!( - "Linux Distribution: {}", - report.system_info.linux_distribution - ); - - println!("\n--- CONFIGURATION ---"); - println!("Current Configuration: {config:#?}"); - - // Print important sysfs paths and whether they exist - println!("\n--- SYSFS PATHS ---"); - check_and_print_sysfs_path( - "/sys/devices/system/cpu/intel_pstate/no_turbo", - "Intel P-State Turbo Control", - ); - check_and_print_sysfs_path( - "/sys/devices/system/cpu/cpufreq/boost", - "Generic CPU Boost Control", - ); - check_and_print_sysfs_path( - "/sys/devices/system/cpu/amd_pstate/cpufreq/boost", - "AMD P-State Boost Control", - ); - check_and_print_sysfs_path( - "/sys/firmware/acpi/platform_profile", - "ACPI Platform Profile Control", - ); - check_and_print_sysfs_path("/sys/class/power_supply", "Power Supply Information"); - - println!("\n--- CPU INFORMATION ---"); - println!("Current Governor: {:?}", report.cpu_global.current_governor); - println!( - "Available Governors: {}", - report.cpu_global.available_governors.join(", ") - ); - println!("Turbo Status: {:?}", report.cpu_global.turbo_status); - println!( - "Energy Performance Preference (EPP): {:?}", - report.cpu_global.epp - ); - println!("Energy Performance Bias (EPB): {:?}", report.cpu_global.epb); - - // Add governor override information - if let Some(override_governor) = cpu::get_governor_override() { - println!("Governor Override: {}", override_governor.trim()); - } else { - println!("Governor Override: None"); - } - - println!("\n--- PLATFORM PROFILE ---"); - println!( - "Current Platform Profile: {:?}", - report.cpu_global.platform_profile - ); - match cpu::get_platform_profiles() { - Ok(profiles) => println!("Available Platform Profiles: {}", profiles.join(", ")), - Err(_) => println!("Available Platform Profiles: Not supported on this system"), - } - - println!("\n--- CPU CORES DETAIL ---"); - println!("Total CPU Cores: {}", report.cpu_cores.len()); - for core in &report.cpu_cores { - println!("Core {}:", core.core_id); - println!( - " Current Frequency: {} MHz", - core.current_frequency_mhz - .map_or_else(|| "N/A".to_string(), |f| f.to_string()) - ); - println!( - " Min Frequency: {} MHz", - core.min_frequency_mhz - .map_or_else(|| "N/A".to_string(), |f| f.to_string()) - ); - println!( - " Max Frequency: {} MHz", - core.max_frequency_mhz - .map_or_else(|| "N/A".to_string(), |f| f.to_string()) - ); - println!( - " Usage: {}%", - core.usage_percent - .map_or_else(|| "N/A".to_string(), |u| format!("{u:.1}")) - ); - println!( - " Temperature: {}°C", - core.temperature_celsius - .map_or_else(|| "N/A".to_string(), |t| format!("{t:.1}")) - ); - } - - println!("\n--- TEMPERATURE INFORMATION ---"); - println!( - "Average CPU Temperature: {}", - report.cpu_global.average_temperature_celsius.map_or_else( - || "N/A (CPU temperature sensor not detected)".to_string(), - |t| format!("{t:.1}°C") - ) - ); - - println!("\n--- BATTERY INFORMATION ---"); - if report.batteries.is_empty() { - println!("No batteries found or all are ignored."); - } else { - for battery in &report.batteries { - println!("Battery: {}", battery.name); - println!(" AC Connected: {}", battery.ac_connected); - println!( - " Charging State: {}", - battery.charging_state.as_deref().unwrap_or("N/A") - ); - println!( - " Capacity: {}%", - battery - .capacity_percent - .map_or_else(|| "N/A".to_string(), |c| c.to_string()) - ); - println!( - " Power Rate: {} W", - battery - .power_rate_watts - .map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}")) - ); - println!( - " Charge Start Threshold: {}", - battery - .charge_start_threshold - .map_or_else(|| "N/A".to_string(), |t| t.to_string()) - ); - println!( - " Charge Stop Threshold: {}", - battery - .charge_stop_threshold - .map_or_else(|| "N/A".to_string(), |t| t.to_string()) - ); - } - } - - println!("\n--- SYSTEM LOAD ---"); - println!( - "Load Average (1 min): {:.2}", - report.system_load.load_avg_1min - ); - println!( - "Load Average (5 min): {:.2}", - report.system_load.load_avg_5min - ); - println!( - "Load Average (15 min): {:.2}", - report.system_load.load_avg_15min - ); - - println!("\n--- DAEMON STATUS ---"); - // Simple check for daemon status - can be expanded later - let daemon_status = fs::metadata("/var/run/superfreq.pid").is_ok(); - println!("Daemon Running: {daemon_status}"); - - // Check for systemd service status - if let Ok(systemd_status) = is_systemd_service_active("superfreq") { - println!("Systemd Service Active: {systemd_status}"); - } - - Ok(()) - } - Err(e) => Err(AppError::Monitor(e)), - } -} - -/// Get kernel version information -fn get_kernel_info() -> Result { - let output = Command::new("uname") - .arg("-r") - .output() - .map_err(AppError::Io)?; - - let kernel_version = String::from_utf8(output.stdout) - .map_err(|e| AppError::Generic(format!("Failed to parse kernel version: {e}")))?; - Ok(kernel_version.trim().to_string()) -} - -/// Get system uptime -fn get_system_uptime() -> Result { - let uptime_str = fs::read_to_string("/proc/uptime").map_err(AppError::Io)?; - let uptime_secs = uptime_str - .split_whitespace() - .next() - .ok_or_else(|| AppError::Generic("Invalid format in /proc/uptime file".to_string()))? - .parse::() - .map_err(|e| AppError::Generic(format!("Failed to parse uptime from /proc/uptime: {e}")))?; - - Ok(Duration::from_secs_f64(uptime_secs)) -} - -/// Check if a sysfs path exists and print its status -fn check_and_print_sysfs_path(path: &str, description: &str) { - let exists = std::path::Path::new(path).exists(); - println!( - "{}: {} ({})", - description, - path, - if exists { "Exists" } else { "Not Found" } - ); -} - -/// Check if a systemd service is active -fn is_systemd_service_active(service_name: &str) -> Result { - let output = Command::new("systemctl") - .arg("is-active") - .arg(format!("{service_name}.service")) - .stdout(Stdio::piped()) // capture stdout instead of letting it print - .stderr(Stdio::null()) // redirect stderr to null - .output() - .map_err(AppError::Io)?; - - // Check if the command executed successfully - if !output.status.success() { - // Command failed - service is either not found or not active - return Ok(false); - } - - // Command executed successfully, now check the output content - let status = String::from_utf8(output.stdout) - .map_err(|e| AppError::Generic(format!("Failed to parse systemctl output: {e}")))?; - - // Explicitly verify the output is "active" - Ok(status.trim() == "active") -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs deleted file mode 100644 index 2f36523..0000000 --- a/src/cli/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod debug; diff --git a/src/config/load.rs b/src/config/load.rs index 51f7e22..15f4248 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -2,7 +2,9 @@ use std::fs; use std::path::{Path, PathBuf}; -use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig}; +use anyhow::Context as _; + +use crate::config::types::{AppConfig, AppConfigToml, DaemonConfig, ProfileConfig}; /// The primary function to load application configuration from a specific path or from default locations. /// @@ -14,22 +16,23 @@ use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, /// /// * `Ok(AppConfig)` - Successfully loaded configuration /// * `Err(ConfigError)` - Error loading or parsing configuration -pub fn load_config() -> Result { +pub fn load_config() -> anyhow::Result { load_config_from_path(None) } /// Load configuration from a specific path or try default paths -pub fn load_config_from_path(specific_path: Option<&str>) -> Result { +pub fn load_config_from_path(specific_path: Option<&str>) -> anyhow::Result { // If a specific path is provided, only try that one if let Some(path_str) = specific_path { let path = Path::new(path_str); if path.exists() { return load_and_parse_config(path); } - return Err(ConfigError::Io(std::io::Error::new( + + Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!("Specified config file not found: {}", path.display()), - ))); + ))?; } // Check for SUPERFREQ_CONFIG environment variable @@ -79,10 +82,16 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result Result { - let contents = fs::read_to_string(path).map_err(ConfigError::Io)?; +fn load_and_parse_config(path: &Path) -> anyhow::Result { + let contents = fs::read_to_string(path).with_context(|| { + format!( + "failed to read config file from '{path}'", + path = path.display(), + ) + })?; - let toml_app_config = toml::from_str::(&contents).map_err(ConfigError::Toml)?; + let toml_app_config = + toml::from_str::(&contents).context("failed to parse config toml")?; // Handle inheritance of values from global to profile configs let mut charger_profile = toml_app_config.charger.clone(); diff --git a/src/config/types.rs b/src/config/types.rs index 3150fc5..c0be6e2 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,16 +1,18 @@ +use anyhow::bail; // Configuration types and structures for superfreq -use crate::core::TurboSetting; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; /// Defines constant-returning functions used for default values. -/// This hopefully reduces repetition since we have way too many default functions -/// that just return constants. +/// This hopefully reduces repetition since we have way too many +/// default functions that just return constants. macro_rules! default_const { - ($name:ident, $type:ty, $value:expr) => { - const fn $name() -> $type { - $value - } + ($($name:ident -> $type:ty = $value:expr;)*) => { + $( + const fn $name() -> $type { + $value + } + )* }; } @@ -20,34 +22,21 @@ pub struct PowerSupplyChargeThresholds { pub stop: u8, } -impl PowerSupplyChargeThresholds { - pub fn new(start: u8, stop: u8) -> Result { +impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { + type Error = anyhow::Error; + + fn try_from((start, stop): (u8, u8)) -> anyhow::Result { if stop == 0 { - return Err(ConfigError::Validation( - "Stop threshold must be greater than 0%".to_string(), - )); + bail!("stop threshold must be greater than 0%"); } if start >= stop { - return Err(ConfigError::Validation(format!( - "Start threshold ({start}) must be less than stop threshold ({stop})" - ))); + bail!("start threshold ({start}) must be less than stop threshold ({stop})"); } if stop > 100 { - return Err(ConfigError::Validation(format!( - "Stop threshold ({stop}) cannot exceed 100%" - ))); + bail!("stop threshold ({stop}) cannot exceed 100%"); } - Ok(Self { start, stop }) - } -} - -impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { - type Error = ConfigError; - - fn try_from(values: (u8, u8)) -> Result { - let (start, stop) = values; - Self::new(start, stop) + Ok(PowerSupplyChargeThresholds { start, stop }) } } @@ -55,7 +44,7 @@ impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ProfileConfig { pub governor: Option, - pub turbo: Option, + pub turbo: Option, pub epp: Option, // Energy Performance Preference (EPP) pub epb: Option, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs pub min_freq_mhz: Option, @@ -73,7 +62,7 @@ impl Default for ProfileConfig { fn default() -> Self { Self { governor: Some("schedutil".to_string()), // common sensible default (?) - turbo: Some(TurboSetting::Auto), + turbo: None, epp: None, // defaults depend on governor and system epb: None, // defaults depend on governor and system min_freq_mhz: None, // no override @@ -97,19 +86,6 @@ pub struct AppConfig { pub daemon: DaemonConfig, } -// Error type for config loading -#[derive(Debug, thiserror::Error)] -pub enum ConfigError { - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - #[error("TOML parsing error: {0}")] - Toml(#[from] toml::de::Error), - - #[error("Configuration validation error: {0}")] - Validation(String), -} - // Intermediate structs for TOML parsing #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ProfileConfigToml { @@ -178,22 +154,14 @@ pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is be pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this pub const DEFAULT_INITIAL_TURBO_STATE: bool = false; // by default, start with turbo disabled -default_const!( - default_load_threshold_high, - f32, - DEFAULT_LOAD_THRESHOLD_HIGH -); -default_const!(default_load_threshold_low, f32, DEFAULT_LOAD_THRESHOLD_LOW); -default_const!( - default_temp_threshold_high, - f32, - DEFAULT_TEMP_THRESHOLD_HIGH -); -default_const!( - default_initial_turbo_state, - bool, - DEFAULT_INITIAL_TURBO_STATE -); +default_const! { + default_load_threshold_high -> f32 = DEFAULT_LOAD_THRESHOLD_HIGH; + default_load_threshold_low -> f32 = DEFAULT_LOAD_THRESHOLD_LOW; + + default_temp_threshold_high -> f32 = DEFAULT_TEMP_THRESHOLD_HIGH; + + default_initial_turbo_state -> bool = DEFAULT_INITIAL_TURBO_STATE; +} impl Default for TurboAutoSettings { fn default() -> Self { @@ -213,10 +181,10 @@ impl From for ProfileConfig { turbo: toml_config .turbo .and_then(|s| match s.to_lowercase().as_str() { - "always" => Some(TurboSetting::Always), - "auto" => Some(TurboSetting::Auto), - "never" => Some(TurboSetting::Never), - _ => None, + "always" => Some(true), + "auto" => None, + "never" => Some(false), + _ => panic!("invalid turbo value: {s}, must be one of: always, auto, never"), }), epp: toml_config.epp, epb: toml_config.epb, @@ -270,14 +238,16 @@ impl Default for DaemonConfig { } } -default_const!(default_poll_interval_sec, u64, 5); -default_const!(default_adaptive_interval, bool, false); -default_const!(default_min_poll_interval_sec, u64, 1); -default_const!(default_max_poll_interval_sec, u64, 30); -default_const!(default_throttle_on_battery, bool, true); -default_const!(default_log_level, LogLevel, LogLevel::Info); -default_const!(default_stats_file_path, Option, None); -default_const!(default_enable_auto_turbo, bool, true); +default_const! { + default_poll_interval_sec -> u64 = 5; + default_adaptive_interval -> bool = false; + default_min_poll_interval_sec -> u64 = 1; + default_max_poll_interval_sec -> u64 = 30; + default_throttle_on_battery -> bool = true; + default_log_level -> LogLevel = LogLevel::Info; + default_stats_file_path -> Option = None; + default_enable_auto_turbo -> bool = true; +} #[derive(Deserialize, Serialize, Debug, Clone)] pub struct DaemonConfigToml { diff --git a/src/daemon.rs b/src/daemon.rs index e2e4fb1..ba6d37d 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,8 +1,10 @@ -use crate::config::{AppConfig, LogLevel}; +use anyhow::Context; +use anyhow::bail; + +use crate::config::AppConfig; use crate::core::SystemReport; use crate::engine; use crate::monitor; -use crate::util::error::{AppError, ControlError}; use std::collections::VecDeque; use std::fs::File; use std::io::Write; @@ -60,10 +62,7 @@ fn idle_multiplier(idle_secs: u64) -> f32 { /// Calculate optimal polling interval based on system conditions and history /// /// Returns Ok with the calculated interval, or Err if the configuration is invalid -fn compute_new( - params: &IntervalParams, - system_history: &SystemHistory, -) -> Result { +fn compute_new(params: &IntervalParams, system_history: &SystemHistory) -> anyhow::Result { // Use the centralized validation function validate_poll_intervals(params.min_interval, params.max_interval)?; @@ -361,7 +360,7 @@ impl SystemHistory { &self, config: &AppConfig, on_battery: bool, - ) -> Result { + ) -> anyhow::Result { let params = IntervalParams { base_interval: config.daemon.poll_interval_sec, min_interval: config.daemon.min_poll_interval_sec, @@ -380,37 +379,31 @@ impl SystemHistory { /// Validates that poll interval configuration is consistent /// Returns Ok if configuration is valid, Err with a descriptive message if invalid -fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), ControlError> { +fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> anyhow::Result<()> { if min_interval < 1 { - return Err(ControlError::InvalidValueError( - "min_interval must be ≥ 1".to_string(), - )); + bail!("min_interval must be ≥ 1"); } if max_interval < 1 { - return Err(ControlError::InvalidValueError( - "max_interval must be ≥ 1".to_string(), - )); + bail!("max_interval must be ≥ 1"); } if max_interval >= min_interval { Ok(()) } else { - Err(ControlError::InvalidValueError(format!( + bail!( "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" - ))) + ); } } /// Run the daemon -pub fn run_daemon(config: AppConfig) -> Result<(), AppError> { +pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { log::info!("Starting superfreq daemon..."); // Validate critical configuration values before proceeding - if let Err(err) = validate_poll_intervals( + validate_poll_intervals( config.daemon.min_poll_interval_sec, config.daemon.max_poll_interval_sec, - ) { - return Err(AppError::Control(err)); - } + )?; // Create a flag that will be set to true when a signal is received let running = Arc::new(AtomicBool::new(true)); @@ -421,7 +414,7 @@ pub fn run_daemon(config: AppConfig) -> Result<(), AppError> { log::info!("Received shutdown signal, exiting..."); r.store(false, Ordering::SeqCst); }) - .map_err(|e| AppError::Generic(format!("Error setting Ctrl-C handler: {e}")))?; + .context("failed to set Ctrl-C handler")?; log::info!( "Daemon initialized with poll interval: {}s", diff --git a/src/engine.rs b/src/engine.rs index 0aa2644..6c5fe59 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,8 +1,7 @@ use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; -use crate::core::{OperationalMode, SystemReport, TurboSetting}; +use crate::core::{OperationalMode, SystemReport}; use crate::cpu::{self}; use crate::power_supply; -use crate::util::error::{ControlError, EngineError}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -119,30 +118,14 @@ impl TurboHysteresis { /// 1. Try to apply a feature setting /// 2. If not supported, log a warning and continue /// 3. If other error, propagate the error -fn try_apply_feature( +fn try_apply_feature anyhow::Result<()>, T>( feature_name: &str, value_description: &str, apply_fn: F, -) -> Result<(), EngineError> -where - F: FnOnce() -> Result, -{ +) -> anyhow::Result<()> { log::info!("Setting {feature_name} to '{value_description}'"); - match apply_fn() { - Ok(_) => Ok(()), - Err(e) => { - if matches!(e, ControlError::NotSupported(_)) { - log::warn!( - "{feature_name} setting is not supported on this system. Skipping {feature_name} configuration." - ); - Ok(()) - } else { - // Propagate all other errors, including InvalidValueError - Err(EngineError::ControlError(e)) - } - } - } + apply_fn() } /// Determines the appropriate CPU profile based on power status or forced mode, @@ -151,19 +134,19 @@ pub fn determine_and_apply_settings( report: &SystemReport, config: &AppConfig, force_mode: Option, -) -> Result<(), EngineError> { - // First, check if there's a governor override set - if let Some(override_governor) = cpu::get_governor_override() { - log::info!( - "Governor override is active: '{}'. Setting governor.", - override_governor.trim() - ); +) -> anyhow::Result<()> { + // // First, check if there's a governor override set + // if let Some(override_governor) = cpu::get_governor_override() { + // log::info!( + // "Governor override is active: '{}'. Setting governor.", + // override_governor.trim() + // ); - // Apply the override governor setting - try_apply_feature("override governor", override_governor.trim(), || { - cpu::set_governor(override_governor.trim(), None) - })?; - } + // // Apply the override governor setting + // try_apply_feature("override governor", override_governor.trim(), || { + // cpu::set_governor(override_governor.trim(), None) + // })?; + // } // Determine AC/Battery status once, early in the function // For desktops (no batteries), we should always use the AC power profile @@ -203,17 +186,11 @@ pub fn determine_and_apply_settings( // Apply settings from selected_profile_config if let Some(governor) = &selected_profile_config.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(_)) - { - log::warn!( - "Configured governor '{governor}' is not available on this system. Skipping." - ); - } else { - return Err(e.into()); + for cpu in cpu::Cpu::all()? { + // Let set_governor handle the validation + if let Err(error) = cpu.set_governor(governor) { + // If the governor is not available, log a warning + log::warn!("{error}"); } } } @@ -297,7 +274,7 @@ fn manage_auto_turbo( report: &SystemReport, config: &ProfileConfig, on_ac_power: bool, -) -> Result<(), EngineError> { +) -> anyhow::Result<()> { // Get the auto turbo settings from the config let turbo_settings = &config.turbo_auto_settings; diff --git a/src/main.rs b/src/main.rs index f4a7120..18341a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod cli; mod config; mod core; mod cpu; @@ -6,7 +5,6 @@ mod daemon; mod engine; mod monitor; mod power_supply; -mod util; use anyhow::Context; use clap::Parser as _; @@ -162,7 +160,7 @@ fn real_main() -> anyhow::Result<()> { } => { let power_supplies = match for_ { Some(names) => { - let power_supplies = Vec::with_capacity(names.len()); + let mut power_supplies = Vec::with_capacity(names.len()); for name in names { power_supplies.push(power_supply::PowerSupply::from_name(name)?); diff --git a/src/monitor.rs b/src/monitor.rs index 5724ae6..79d2635 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,7 +1,5 @@ use crate::config::AppConfig; use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; -use crate::cpu::get_real_cpus; -use crate::util::error::SysMonitorError; use std::{ collections::HashMap, fs, @@ -12,10 +10,8 @@ use std::{ time::SystemTime, }; -pub type Result = std::result::Result; - // Read a sysfs file to a string, trimming whitespace -fn read_sysfs_file_trimmed(path: impl AsRef) -> Result { +fn read_sysfs_file_trimmed(path: impl AsRef) -> anyhow::Result { fs::read_to_string(path.as_ref()) .map(|s| s.trim().to_string()) .map_err(|e| { @@ -24,7 +20,7 @@ fn read_sysfs_file_trimmed(path: impl AsRef) -> Result { } // Read a sysfs file and parse it to a specific type -fn read_sysfs_value(path: impl AsRef) -> Result { +fn read_sysfs_value(path: impl AsRef) -> anyhow::Result { let content = read_sysfs_file_trimmed(path.as_ref())?; content.parse::().map_err(|_| { SysMonitorError::ParseError(format!( @@ -76,7 +72,7 @@ impl CpuTimes { } } -fn read_all_cpu_times() -> Result> { +fn read_all_cpu_times() -> anyhow::Result> { let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?; let mut cpu_times_map = HashMap::new(); @@ -156,7 +152,7 @@ pub fn get_cpu_core_info( core_id: u32, prev_times: &CpuTimes, current_times: &CpuTimes, -) -> Result { +) -> anyhow::Result { let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/")); let current_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_cur_freq")) @@ -358,7 +354,7 @@ fn get_fallback_temperature(hw_path: &Path) -> Option { None } -pub fn get_all_cpu_core_info() -> Result> { +pub fn get_all_cpu_core_info() -> anyhow::Result> { let initial_cpu_times = read_all_cpu_times()?; thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation let final_cpu_times = read_all_cpu_times()?; @@ -486,7 +482,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { } } -pub fn get_battery_info(config: &AppConfig) -> Result> { +pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { let mut batteries = Vec::new(); let power_supply_path = Path::new("/sys/class/power_supply"); @@ -682,7 +678,7 @@ fn is_likely_desktop_system() -> bool { true } -pub fn get_system_load() -> Result { +pub fn get_system_load() -> anyhow::Result { let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); if parts.len() < 3 { @@ -707,7 +703,7 @@ pub fn get_system_load() -> Result { }) } -pub fn collect_system_report(config: &AppConfig) -> Result { +pub fn collect_system_report(config: &AppConfig) -> anyhow::Result { let system_info = get_system_info(); let cpu_cores = get_all_cpu_core_info()?; let cpu_global = get_cpu_global_info(&cpu_cores); @@ -724,7 +720,7 @@ pub fn collect_system_report(config: &AppConfig) -> Result { }) } -pub fn get_cpu_model() -> Result { +pub fn get_cpu_model() -> anyhow::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())) @@ -743,7 +739,7 @@ pub fn get_cpu_model() -> Result { )) } -pub fn get_linux_distribution() -> Result { +pub fn get_linux_distribution() -> anyhow::Result { let os_release_path = Path::new("/etc/os-release"); let content = fs::read_to_string(os_release_path).map_err(|_| { SysMonitorError::ReadError(format!( diff --git a/src/util/error.rs b/src/util/error.rs deleted file mode 100644 index b91081f..0000000 --- a/src/util/error.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::io; - -#[derive(Debug, thiserror::Error)] -pub enum ControlError { - #[error("I/O error: {0}")] - Io(#[from] io::Error), - - #[error("Failed to write to sysfs path: {0}")] - WriteError(String), - - #[error("Failed to read sysfs path: {0}")] - ReadError(String), - - #[error("Invalid value for setting: {0}")] - InvalidValueError(String), - - #[error("Control action not supported: {0}")] - NotSupported(String), - - #[error("Permission denied: {0}. Try running with sudo.")] - PermissionDenied(String), - - #[error("Invalid platform control profile {0} supplied, please provide a valid one.")] - InvalidProfile(String), - - #[error("Invalid governor: {0}")] - InvalidGovernor(String), - - #[error("Failed to parse value: {0}")] - ParseError(String), - - #[error("Path missing: {0}")] - PathMissing(String), -} - -#[derive(Debug, thiserror::Error)] -pub enum SysMonitorError { - #[error("I/O error: {0}")] - Io(#[from] io::Error), - - #[error("Failed to read sysfs path: {0}")] - ReadError(String), - - #[error("Failed to parse value: {0}")] - ParseError(String), - - #[error("Failed to parse /proc/stat: {0}")] - ProcStatParseError(String), -} - -#[derive(Debug, thiserror::Error)] -pub enum EngineError { - #[error("CPU control error: {0}")] - ControlError(#[from] ControlError), - - #[error("Configuration error: {0}")] - ConfigurationError(String), -} - -// A unified error type for the entire application -#[derive(Debug, thiserror::Error)] -pub enum AppError { - #[error("{0}")] - Control(#[from] ControlError), - - #[error("{0}")] - Monitor(#[from] SysMonitorError), - - #[error("{0}")] - Engine(#[from] EngineError), - - #[error("{0}")] - Config(#[from] crate::config::ConfigError), - - #[error("{0}")] - Generic(String), - - #[error("I/O error: {0}")] - Io(#[from] io::Error), -} diff --git a/src/util/mod.rs b/src/util/mod.rs deleted file mode 100644 index 0aa2927..0000000 --- a/src/util/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod sysfs; diff --git a/src/util/sysfs.rs b/src/util/sysfs.rs deleted file mode 100644 index e1776e5..0000000 --- a/src/util/sysfs.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::util::error::ControlError; -use std::{fs, io, path::Path}; - -/// Write a value to a sysfs file with consistent error handling -/// -/// # Arguments -/// -/// * `path` - The file path to write to -/// * `value` - The string value to write -/// -/// # Errors -/// -/// Returns a `ControlError` variant based on the specific error: -/// - `ControlError::PermissionDenied` if permission is denied -/// - `ControlError::PathMissing` if the path doesn't exist -/// - `ControlError::WriteError` for other I/O errors -pub fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<(), ControlError> { - let p = 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), - } - }) -} - -/// Read a value from a sysfs file with consistent error handling -/// -/// # Arguments -/// -/// * `path` - The file path to read from -/// -/// # Returns -/// -/// Returns the trimmed contents of the file as a String -/// -/// # Errors -/// -/// Returns a `ControlError` variant based on the specific error: -/// - `ControlError::PermissionDenied` if permission is denied -/// - `ControlError::PathMissing` if the path doesn't exist -/// - `ControlError::ReadError` for other I/O errors -pub fn read_sysfs_value(path: impl AsRef) -> Result { - let p = path.as_ref(); - fs::read_to_string(p) - .map_err(|e| { - let error_msg = format!("Path: {:?}, Error: {}", p.display(), e); - match e.kind() { - io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(error_msg), - io::ErrorKind::NotFound => { - ControlError::PathMissing(format!("Path '{}' does not exist", p.display())) - } - _ => ControlError::ReadError(error_msg), - } - }) - .map(|s| s.trim().to_string()) -} - -/// Safely check if a path exists and is writable -/// -/// # Arguments -/// -/// * `path` - The file path to check -/// -/// # Returns -/// -/// Returns true if the path exists and is writable, false otherwise -pub fn path_exists_and_writable(path: &Path) -> bool { - if !path.exists() { - return false; - } - - // Try to open the file with write access to verify write permission - fs::OpenOptions::new().write(true).open(path).is_ok() -} From 5559d08f3e065787efc7203f967449a377cda5ce Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:59:44 +0300 Subject: [PATCH 5/7] main: delete historical logging code --- src/main.rs | 261 ---------------------------------------------------- 1 file changed, 261 deletions(-) diff --git a/src/main.rs b/src/main.rs index 18341a6..9902b79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -192,267 +192,6 @@ fn real_main() -> anyhow::Result<()> { 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() { From c69aba87b66123ca0406af4212d135d944cb1226 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 18:07:35 +0300 Subject: [PATCH 6/7] power_supply: add derives to PowerSupply --- src/power_supply.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/power_supply.rs b/src/power_supply.rs index 92147da..06d3ec8 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -53,6 +53,7 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ ]; /// Represents a power supply that supports charge threshold control. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PowerSupply { pub name: String, pub path: PathBuf, From cc0cc23b0d2b74d4b1a4dd218e0e7318b3bc27f5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 18:08:52 +0300 Subject: [PATCH 7/7] power_supply: rename is_battery to get_type and don't compare the type --- src/power_supply.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/power_supply.rs b/src/power_supply.rs index 06d3ec8..9ec00ab 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -137,19 +137,20 @@ impl PowerSupply { Ok(power_supplies) } - fn is_battery(&self) -> anyhow::Result { + fn get_type(&self) -> anyhow::Result { let type_path = self.path.join("type"); let type_ = fs::read_to_string(&type_path) .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - Ok(type_ == "Battery") + Ok(type_) } pub fn rescan(&mut self) -> anyhow::Result<()> { let threshold_config = self - .is_battery() + .get_type() .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? + .eq("Battery") .then(|| { for config in POWER_SUPPLY_THRESHOLD_CONFIGS { if self.path.join(config.path_start).exists()