From 76370a74456e8064b66989ccdb6869db56617956 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 17:26:48 +0300 Subject: [PATCH 01/93] cpu: clean up, clean main too --- Cargo.lock | 62 ++++ Cargo.toml | 3 + src/battery.rs | 31 +- src/core.rs | 28 -- src/cpu.rs | 815 ++++++++++++++++++---------------------------- src/daemon.rs | 71 ++-- src/engine.rs | 68 ++-- src/main.rs | 869 +++++++++++++++++++++++++------------------------ src/monitor.rs | 11 +- 9 files changed, 904 insertions(+), 1054 deletions(-) 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) From 8764d3a2ac741af8e91787f1be23c9fa101a038c Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:12:18 +0300 Subject: [PATCH 02/93] battery: clean up, rename to power_supply --- src/battery.rs | 267 -------------------------------------------- src/config/types.rs | 12 +- src/engine.rs | 4 +- src/main.rs | 27 ++--- src/power_supply.rs | 175 +++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 295 deletions(-) delete mode 100644 src/battery.rs create mode 100644 src/power_supply.rs diff --git a/src/battery.rs b/src/battery.rs deleted file mode 100644 index b15e6bb..0000000 --- a/src/battery.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs}; -use std::{ - fs, io, - path::{Path, PathBuf}, -}; - -pub type Result = std::result::Result; - -/// Represents a pattern of path suffixes used to control battery charge thresholds -/// for different device vendors. -#[derive(Clone)] -pub struct ThresholdPathPattern { - pub description: &'static str, - pub start_path: &'static str, - pub stop_path: &'static str, -} - -// Threshold patterns -const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[ - ThresholdPathPattern { - description: "Standard", - start_path: "charge_control_start_threshold", - stop_path: "charge_control_end_threshold", - }, - ThresholdPathPattern { - description: "ASUS", - start_path: "charge_control_start_percentage", - stop_path: "charge_control_end_percentage", - }, - // Combine Huawei and ThinkPad since they use identical paths - ThresholdPathPattern { - description: "ThinkPad/Huawei", - start_path: "charge_start_threshold", - stop_path: "charge_stop_threshold", - }, - // Framework laptop support - ThresholdPathPattern { - description: "Framework", - start_path: "charge_behaviour_start_threshold", - stop_path: "charge_behaviour_end_threshold", - }, -]; - -/// Represents a battery that supports charge threshold control -pub struct SupportedBattery<'a> { - pub name: String, - pub pattern: &'a ThresholdPathPattern, - pub path: PathBuf, -} - -/// Set battery charge thresholds to protect battery health -/// -/// This sets the start and stop charging thresholds for batteries that support this feature. -/// Different laptop vendors implement battery thresholds in different ways, so this function -/// attempts to handle multiple implementations (Lenovo, ASUS, etc.). -/// -/// The thresholds determine at what percentage the battery starts charging (when below `start_threshold`) -/// and at what percentage it stops (when it reaches `stop_threshold`). -/// -/// # Arguments -/// -/// * `start_threshold` - The battery percentage at which charging should start (typically 0-99) -/// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100) -/// -/// # Errors -/// -/// Returns an error if: -/// - The thresholds are invalid (start >= stop or stop > 100) -/// - No power supply path is found -/// - No batteries with threshold support are found -/// - Failed to set thresholds on any battery -pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> { - // Validate thresholds using `BatteryChargeThresholds` - let thresholds = - BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e { - crate::config::types::ConfigError::Validation(msg) => { - ControlError::InvalidValueError(msg) - } - _ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")), - })?; - - let power_supply_path = Path::new("/sys/class/power_supply"); - if !power_supply_path.exists() { - return Err(ControlError::NotSupported( - "Power supply path not found, battery threshold control not supported".to_string(), - )); - } - - // XXX: Skip checking directory writability since /sys is a virtual filesystem - // Individual file writability will be checked by find_battery_with_threshold_support - - let supported_batteries = find_supported_batteries(power_supply_path)?; - if supported_batteries.is_empty() { - return Err(ControlError::NotSupported( - "No batteries with charge threshold control support found".to_string(), - )); - } - - apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop) -} - -/// Finds all batteries in the system that support threshold control -fn find_supported_batteries(power_supply_path: &Path) -> Result>> { - let entries = fs::read_dir(power_supply_path).map_err(|e| { - if e.kind() == io::ErrorKind::PermissionDenied { - ControlError::PermissionDenied(format!( - "Permission denied accessing power supply directory: {}", - power_supply_path.display() - )) - } else { - ControlError::Io(e) - } - })?; - - let mut supported_batteries = Vec::new(); - for entry in entries { - let entry = match entry { - Ok(e) => e, - Err(e) => { - log::warn!("Failed to read power-supply entry: {e}"); - continue; - } - }; - let ps_path = entry.path(); - if is_battery(&ps_path)? { - if let Some(battery) = find_battery_with_threshold_support(&ps_path) { - supported_batteries.push(battery); - } - } - } - - if supported_batteries.is_empty() { - log::warn!("No batteries with charge threshold support found"); - } else { - log::debug!( - "Found {} batteries with threshold support", - supported_batteries.len() - ); - for battery in &supported_batteries { - log::debug!( - "Battery '{}' supports {} threshold control", - battery.name, - battery.pattern.description - ); - } - } - - Ok(supported_batteries) -} - -/// Applies the threshold settings to all supported batteries -fn apply_thresholds_to_batteries( - batteries: &[SupportedBattery<'_>], - start_threshold: u8, - stop_threshold: u8, -) -> Result<()> { - let mut errors = Vec::new(); - let mut success_count = 0; - - for battery in batteries { - let start_path = battery.path.join(battery.pattern.start_path); - let stop_path = battery.path.join(battery.pattern.stop_path); - - // Read current thresholds in case we need to restore them - let current_stop = sysfs::read_sysfs_value(&stop_path).ok(); - - // Write stop threshold first (must be >= start threshold) - let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string()); - - // Only proceed to set start threshold if stop threshold was set successfully - if matches!(stop_result, Ok(())) { - let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string()); - - match start_result { - Ok(()) => { - log::debug!( - "Set {}-{}% charge thresholds for {} battery '{}'", - start_threshold, - stop_threshold, - battery.pattern.description, - battery.name - ); - success_count += 1; - } - Err(e) => { - // Start threshold failed, try to restore the previous stop threshold - if let Some(prev_stop) = ¤t_stop { - let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop); - if let Err(re) = restore_result { - log::warn!( - "Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.", - battery.name, - re - ); - } else { - log::debug!( - "Restored previous stop threshold ({}) for battery '{}'", - prev_stop, - battery.name - ); - } - } - - errors.push(format!( - "Failed to set start threshold for {} battery '{}': {}", - battery.pattern.description, battery.name, e - )); - } - } - } else if let Err(e) = stop_result { - errors.push(format!( - "Failed to set stop threshold for {} battery '{}': {}", - battery.pattern.description, battery.name, e - )); - } - } - - if success_count > 0 { - if !errors.is_empty() { - log::warn!( - "Partial success setting battery thresholds: {}", - errors.join("; ") - ); - } - Ok(()) - } else { - Err(ControlError::WriteError(format!( - "Failed to set charge thresholds on any battery: {}", - errors.join("; ") - ))) - } -} - -/// Determines if a power supply entry is a battery -fn is_battery(path: &Path) -> Result { - let type_path = path.join("type"); - - if !type_path.exists() { - return Ok(false); - } - - let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| { - ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e)) - })?; - - Ok(ps_type == "Battery") -} - -/// Identifies if a battery supports threshold control and which pattern it uses -fn find_battery_with_threshold_support(ps_path: &Path) -> Option> { - for pattern in THRESHOLD_PATTERNS { - let start_threshold_path = ps_path.join(pattern.start_path); - let stop_threshold_path = ps_path.join(pattern.stop_path); - - // Ensure both paths exist and are writable before considering this battery supported - if sysfs::path_exists_and_writable(&start_threshold_path) - && sysfs::path_exists_and_writable(&stop_threshold_path) - { - return Some(SupportedBattery { - name: ps_path.file_name()?.to_string_lossy().to_string(), - pattern, - path: ps_path.to_path_buf(), - }); - } - } - None -} diff --git a/src/config/types.rs b/src/config/types.rs index eb9ce7f..3150fc5 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -15,12 +15,12 @@ macro_rules! default_const { } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct BatteryChargeThresholds { +pub struct PowerSupplyChargeThresholds { pub start: u8, pub stop: u8, } -impl BatteryChargeThresholds { +impl PowerSupplyChargeThresholds { pub fn new(start: u8, stop: u8) -> Result { if stop == 0 { return Err(ConfigError::Validation( @@ -42,7 +42,7 @@ impl BatteryChargeThresholds { } } -impl TryFrom<(u8, u8)> for BatteryChargeThresholds { +impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { type Error = ConfigError; fn try_from(values: (u8, u8)) -> Result { @@ -66,7 +66,7 @@ pub struct ProfileConfig { #[serde(default)] pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, } impl Default for ProfileConfig { @@ -124,7 +124,7 @@ pub struct ProfileConfigToml { #[serde(default = "default_enable_auto_turbo")] pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, } #[derive(Deserialize, Serialize, Debug, Clone, Default)] @@ -134,7 +134,7 @@ pub struct AppConfigToml { #[serde(default)] pub battery: ProfileConfigToml, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, pub ignored_power_supplies: Option>, #[serde(default)] pub daemon: DaemonConfigToml, diff --git a/src/engine.rs b/src/engine.rs index fadff3b..0aa2644 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,7 @@ -use crate::battery; use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self}; +use crate::power_supply; use crate::util::error::{ControlError, EngineError}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -277,7 +277,7 @@ pub fn determine_and_apply_settings( if start_threshold < stop_threshold && stop_threshold <= 100 { log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); - match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { + match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) { Ok(()) => log::debug!("Battery charge thresholds set successfully"), Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), } diff --git a/src/main.rs b/src/main.rs index 333cf64..bc4b0c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod battery; mod cli; mod config; mod core; @@ -6,6 +5,7 @@ mod cpu; mod daemon; mod engine; mod monitor; +mod power_supply; mod util; use anyhow::{Context, anyhow, bail}; @@ -148,27 +148,14 @@ fn real_main() -> anyhow::Result<()> { cpu::set_platform_profile(platform_profile)?; } - // 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") - })?; - - 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})" - ); + for power_supply in power_supply::get_power_supplies()? { + if let Some(threshold_start) = charge_threshold_start { + power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; } - battery::set_battery_charge_thresholds( - charge_threshold_start, - charge_threshold_end, - )?; + if let Some(threshold_end) = charge_threshold_end { + power_supply::set_charge_threshold_end(&power_supply, threshold_end)?; + } } Ok(()) diff --git a/src/power_supply.rs b/src/power_supply.rs new file mode 100644 index 0000000..7320f18 --- /dev/null +++ b/src/power_supply.rs @@ -0,0 +1,175 @@ +use anyhow::Context; + +use std::{ + fmt, fs, + 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 manufacturer: &'static str, + pub path_start: &'static str, + pub path_end: &'static str, +} + +/// Charge threshold configs. +const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ + PowerSupplyConfig { + manufacturer: "Standard", + path_start: "charge_control_start_threshold", + path_end: "charge_control_end_threshold", + }, + PowerSupplyConfig { + manufacturer: "ASUS", + path_start: "charge_control_start_percentage", + path_end: "charge_control_end_percentage", + }, + // Combine Huawei and ThinkPad since they use identical paths. + PowerSupplyConfig { + manufacturer: "ThinkPad/Huawei", + path_start: "charge_start_threshold", + path_end: "charge_stop_threshold", + }, + // Framework laptop support. + PowerSupplyConfig { + manufacturer: "Framework", + path_start: "charge_behaviour_start_threshold", + path_end: "charge_behaviour_end_threshold", + }, +]; + +/// Represents a power supply that supports charge threshold control. +pub struct PowerSupply { + pub name: String, + pub path: PathBuf, + pub config: PowerSupplyConfig, +} + +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, + ) + } +} + +impl PowerSupply { + pub fn charge_threshold_path_start(&self) -> PathBuf { + self.path.join(self.config.path_start) + } + + pub fn charge_threshold_path_end(&self) -> PathBuf { + self.path.join(self.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(), + ) + }) +} + +fn is_power_supply(path: &Path) -> anyhow::Result { + let type_path = 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") +} + +/// Get all batteries in the system that support threshold control. +pub fn get_power_supplies() -> anyhow::Result> { + const PATH: &str = "/sys/class/power_supply"; + + let mut power_supplies = Vec::new(); + + 'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{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(); + + if !is_power_supply(&entry_path).with_context(|| { + format!( + "failed to determine whether if '{path}' is a power supply", + 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; + } + } + } + + 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(), + &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(), + &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(()) +} From a1502009d59a29a3f15c51ec69e215eff0b909c8 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:17:49 +0300 Subject: [PATCH 03/93] cli: remove governor_persist --- src/main.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index bc4b0c1..b86b414 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod monitor; mod power_supply; mod util; -use anyhow::{Context, anyhow, bail}; +use anyhow::Context; use clap::Parser as _; use std::fmt::Write as _; use std::io::Write as _; @@ -43,10 +43,6 @@ enum Command { #[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, @@ -103,7 +99,6 @@ fn real_main() -> anyhow::Result<()> { Command::Set { for_, governor, - governor_persist, energy_performance_preference, energy_performance_bias, frequency_mhz_minimum, From 8f3abd1ca3a4bc80c8731d4883ab976c5f0024d5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:38:44 +0300 Subject: [PATCH 04/93] 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 05/93] 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 06/93] 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 07/93] 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 08/93] 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 09/93] 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 10/93] 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() From 98bbf28f3d1200a27457c80014ea0fe4076427ab Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:25:26 +0300 Subject: [PATCH 11/93] config: nuke old config and implement a new system --- Cargo.toml | 2 +- src/config.rs | 138 ++++++++++++++++++++++ src/config/load.rs | 128 -------------------- src/config/mod.rs | 5 - src/config/types.rs | 282 -------------------------------------------- src/cpu.rs | 4 +- src/main.rs | 73 +++--------- 7 files changed, 158 insertions(+), 474 deletions(-) create mode 100644 src/config.rs delete mode 100644 src/config/load.rs delete mode 100644 src/config/mod.rs delete mode 100644 src/config/types.rs diff --git a/Cargo.toml b/Cargo.toml index 3276b4a..287929e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.85" serde = { version = "1.0", features = ["derive"] } toml = "0.8" dirs = "6.0" -clap = { version = "4.0", features = ["derive"] } +clap = { version = "4.0", features = ["derive", "env"] } num_cpus = "1.16" ctrlc = "3.4" log = "0.4" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0e07031 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,138 @@ +use std::{fs, path::Path}; + +use anyhow::{Context, bail}; +use serde::{Deserialize, Serialize}; + +fn is_default(value: &T) -> bool { + *value == T::default() +} + +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] +pub struct CpuDelta { + /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. + #[arg(short = 'c', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, + + /// Set the CPU governor. + #[arg(short = 'g', long)] + #[serde(skip_serializing_if = "is_default")] + pub governor: Option, // TODO: Validate with clap for available governors. + + /// Set CPU Energy Performance Preference (EPP). Short form: --epp. + #[arg(short = 'p', long, alias = "epp")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_preference: Option, // TODO: Validate with clap for available governors. + + /// Set CPU Energy Performance Bias (EPB). Short form: --epb. + #[arg(short = 'b', long, alias = "epb")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_bias: Option, // TODO: Validate with clap for available governors. + + /// 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))] + #[serde(skip_serializing_if = "is_default")] + pub 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))] + #[serde(skip_serializing_if = "is_default")] + pub frequency_mhz_maximum: Option, + + /// Set turbo boost behaviour. Has to be for all CPUs. + #[arg(short = 't', long, conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub turbo: Option, +} + +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] +pub struct PowerDelta { + /// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. + #[arg(short = 'p', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, + + /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. + #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_start: Option, + + /// Set the percentage where charging will stop. Short form: --charge-end. + #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_end: Option, + + /// Set ACPI platform profile. Has to be for all power supplies. + #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub platform_profile: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(untagged, rename_all = "kebab-case")] +pub enum Condition { + ChargeLessThan(u8), + ChargeMoreThan(u8), + + TemperatureLessThan(u8), + TemperatureMoreThan(u8), + + UtilizationLessThan(u8), + UtilizationMoreThan(u8), + + Charging, + OnBattery, + + False, + #[default] + True, + + All(Vec), + Any(Vec), + + Not(Box), +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct DaemonConfigLayer { + priority: u8, + + #[serde(default, skip_serializing_if = "is_default")] + if_: Condition, + + #[serde(default, skip_serializing_if = "is_default")] + cpu: CpuDelta, + #[serde(default, skip_serializing_if = "is_default")] + power: PowerDelta, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(transparent, default, rename_all = "kebab-case")] +pub struct DaemonConfig(pub Vec); + +impl DaemonConfig { + pub fn load_from(path: &Path) -> anyhow::Result { + let contents = fs::read_to_string(path).with_context(|| { + format!("failed to read config from '{path}'", path = path.display()) + })?; + + let config: Self = toml::from_str(&contents).context("failed to parse config file")?; + + { + let mut priorities = Vec::with_capacity(config.0.len()); + + for layer in &config.0 { + if priorities.contains(&layer.priority) { + bail!("each config layer must have a different priority") + } + + priorities.push(layer.priority); + } + } + + Ok(config) + } +} diff --git a/src/config/load.rs b/src/config/load.rs deleted file mode 100644 index 15f4248..0000000 --- a/src/config/load.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Configuration loading functionality -use std::fs; -use std::path::{Path, PathBuf}; - -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. -/// -/// # Arguments -/// -/// * `specific_path` - If provided, only attempts to load from this path and errors if not found -/// -/// # Returns -/// -/// * `Ok(AppConfig)` - Successfully loaded configuration -/// * `Err(ConfigError)` - Error loading or parsing configuration -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>) -> 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); - } - - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Specified config file not found: {}", path.display()), - ))?; - } - - // Check for SUPERFREQ_CONFIG environment variable - if let Ok(env_path) = std::env::var("SUPERFREQ_CONFIG") { - let env_path = Path::new(&env_path); - if env_path.exists() { - println!( - "Loading config from SUPERFREQ_CONFIG: {}", - env_path.display() - ); - return load_and_parse_config(env_path); - } - eprintln!( - "Warning: Config file specified by SUPERFREQ_CONFIG not found: {}", - env_path.display() - ); - } - - // System-wide paths - let config_paths = vec![ - PathBuf::from("/etc/xdg/superfreq/config.toml"), - PathBuf::from("/etc/superfreq.toml"), - ]; - - for path in config_paths { - if path.exists() { - println!("Loading config from: {}", path.display()); - match load_and_parse_config(&path) { - Ok(config) => return Ok(config), - Err(e) => { - eprintln!("Error with config file {}: {}", path.display(), e); - // Continue trying other files - } - } - } - } - - println!("No configuration file found or all failed to parse. Using default configuration."); - // Construct default AppConfig by converting default AppConfigToml - let default_toml_config = AppConfigToml::default(); - Ok(AppConfig { - charger: ProfileConfig::from(default_toml_config.charger), - battery: ProfileConfig::from(default_toml_config.battery), - ignored_power_supplies: default_toml_config.ignored_power_supplies, - daemon: DaemonConfig::default(), - }) -} - -/// Load and parse a configuration file -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).context("failed to parse config toml")?; - - // Handle inheritance of values from global to profile configs - let mut charger_profile = toml_app_config.charger.clone(); - let mut battery_profile = toml_app_config.battery.clone(); - - // Clone global battery_charge_thresholds once if it exists - if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds { - // Apply to charger profile if not already set - if charger_profile.battery_charge_thresholds.is_none() { - charger_profile.battery_charge_thresholds = Some(global_thresholds.clone()); - } - - // Apply to battery profile if not already set - if battery_profile.battery_charge_thresholds.is_none() { - battery_profile.battery_charge_thresholds = Some(global_thresholds); - } - } - - // Convert AppConfigToml to AppConfig - Ok(AppConfig { - charger: ProfileConfig::from(charger_profile), - battery: ProfileConfig::from(battery_profile), - ignored_power_supplies: toml_app_config.ignored_power_supplies, - daemon: DaemonConfig { - poll_interval_sec: toml_app_config.daemon.poll_interval_sec, - adaptive_interval: toml_app_config.daemon.adaptive_interval, - min_poll_interval_sec: toml_app_config.daemon.min_poll_interval_sec, - max_poll_interval_sec: toml_app_config.daemon.max_poll_interval_sec, - throttle_on_battery: toml_app_config.daemon.throttle_on_battery, - log_level: toml_app_config.daemon.log_level, - stats_file_path: toml_app_config.daemon.stats_file_path, - }, - }) -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index c2f3076..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod load; -pub mod types; - -pub use load::*; -pub use types::*; diff --git a/src/config/types.rs b/src/config/types.rs deleted file mode 100644 index c0be6e2..0000000 --- a/src/config/types.rs +++ /dev/null @@ -1,282 +0,0 @@ -use anyhow::bail; -// Configuration types and structures for superfreq -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. -macro_rules! default_const { - ($($name:ident -> $type:ty = $value:expr;)*) => { - $( - const fn $name() -> $type { - $value - } - )* - }; -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct PowerSupplyChargeThresholds { - pub start: u8, - pub stop: u8, -} - -impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { - type Error = anyhow::Error; - - fn try_from((start, stop): (u8, u8)) -> anyhow::Result { - if stop == 0 { - bail!("stop threshold must be greater than 0%"); - } - if start >= stop { - bail!("start threshold ({start}) must be less than stop threshold ({stop})"); - } - if stop > 100 { - bail!("stop threshold ({stop}) cannot exceed 100%"); - } - - Ok(PowerSupplyChargeThresholds { start, stop }) - } -} - -// Structs for configuration using serde::Deserialize -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ProfileConfig { - pub governor: 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, - pub max_freq_mhz: Option, - pub platform_profile: Option, - #[serde(default)] - pub turbo_auto_settings: TurboAutoSettings, - #[serde(default)] - pub enable_auto_turbo: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, -} - -impl Default for ProfileConfig { - fn default() -> Self { - Self { - governor: Some("schedutil".to_string()), // common sensible default (?) - turbo: None, - epp: None, // defaults depend on governor and system - epb: None, // defaults depend on governor and system - min_freq_mhz: None, // no override - max_freq_mhz: None, // no override - platform_profile: None, // no override - turbo_auto_settings: TurboAutoSettings::default(), - enable_auto_turbo: default_enable_auto_turbo(), - battery_charge_thresholds: None, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -pub struct AppConfig { - #[serde(default)] - pub charger: ProfileConfig, - #[serde(default)] - pub battery: ProfileConfig, - pub ignored_power_supplies: Option>, - #[serde(default)] - pub daemon: DaemonConfig, -} - -// Intermediate structs for TOML parsing -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ProfileConfigToml { - pub governor: Option, - pub turbo: Option, // "always", "auto", "never" - pub epp: Option, - pub epb: Option, - pub min_freq_mhz: Option, - pub max_freq_mhz: Option, - pub platform_profile: Option, - pub turbo_auto_settings: Option, - #[serde(default = "default_enable_auto_turbo")] - pub enable_auto_turbo: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone, Default)] -pub struct AppConfigToml { - #[serde(default)] - pub charger: ProfileConfigToml, - #[serde(default)] - pub battery: ProfileConfigToml, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, - pub ignored_power_supplies: Option>, - #[serde(default)] - pub daemon: DaemonConfigToml, -} - -impl Default for ProfileConfigToml { - fn default() -> Self { - Self { - governor: Some("schedutil".to_string()), - turbo: Some("auto".to_string()), - epp: None, - epb: None, - min_freq_mhz: None, - max_freq_mhz: None, - platform_profile: None, - turbo_auto_settings: None, - enable_auto_turbo: default_enable_auto_turbo(), - battery_charge_thresholds: None, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct TurboAutoSettings { - #[serde(default = "default_load_threshold_high")] - pub load_threshold_high: f32, - #[serde(default = "default_load_threshold_low")] - pub load_threshold_low: f32, - #[serde(default = "default_temp_threshold_high")] - pub temp_threshold_high: f32, - /// Initial turbo boost state when no previous state exists. - /// Set to `true` to start with turbo enabled, `false` to start with turbo disabled. - /// This is only used at first launch or after a reset. - #[serde(default = "default_initial_turbo_state")] - pub initial_turbo_state: bool, -} - -// Default thresholds for Auto turbo mode -pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this -pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this -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_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 { - Self { - load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, - load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, - temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, - initial_turbo_state: DEFAULT_INITIAL_TURBO_STATE, - } - } -} - -impl From for ProfileConfig { - fn from(toml_config: ProfileConfigToml) -> Self { - Self { - governor: toml_config.governor, - turbo: toml_config - .turbo - .and_then(|s| match s.to_lowercase().as_str() { - "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, - min_freq_mhz: toml_config.min_freq_mhz, - max_freq_mhz: toml_config.max_freq_mhz, - platform_profile: toml_config.platform_profile, - turbo_auto_settings: toml_config.turbo_auto_settings.unwrap_or_default(), - enable_auto_turbo: toml_config.enable_auto_turbo, - battery_charge_thresholds: toml_config.battery_charge_thresholds, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct DaemonConfig { - #[serde(default = "default_poll_interval_sec")] - pub poll_interval_sec: u64, - #[serde(default = "default_adaptive_interval")] - pub adaptive_interval: bool, - #[serde(default = "default_min_poll_interval_sec")] - pub min_poll_interval_sec: u64, - #[serde(default = "default_max_poll_interval_sec")] - pub max_poll_interval_sec: u64, - #[serde(default = "default_throttle_on_battery")] - pub throttle_on_battery: bool, - #[serde(default = "default_log_level")] - pub log_level: LogLevel, - #[serde(default = "default_stats_file_path")] - pub stats_file_path: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogLevel { - Error, - Warning, - Info, - Debug, -} - -impl Default for DaemonConfig { - fn default() -> Self { - Self { - poll_interval_sec: default_poll_interval_sec(), - adaptive_interval: default_adaptive_interval(), - min_poll_interval_sec: default_min_poll_interval_sec(), - max_poll_interval_sec: default_max_poll_interval_sec(), - throttle_on_battery: default_throttle_on_battery(), - log_level: default_log_level(), - stats_file_path: default_stats_file_path(), - } - } -} - -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 { - #[serde(default = "default_poll_interval_sec")] - pub poll_interval_sec: u64, - #[serde(default = "default_adaptive_interval")] - pub adaptive_interval: bool, - #[serde(default = "default_min_poll_interval_sec")] - pub min_poll_interval_sec: u64, - #[serde(default = "default_max_poll_interval_sec")] - pub max_poll_interval_sec: u64, - #[serde(default = "default_throttle_on_battery")] - pub throttle_on_battery: bool, - #[serde(default = "default_log_level")] - pub log_level: LogLevel, - #[serde(default = "default_stats_file_path")] - pub stats_file_path: Option, -} - -impl Default for DaemonConfigToml { - fn default() -> Self { - Self { - poll_interval_sec: default_poll_interval_sec(), - adaptive_interval: default_adaptive_interval(), - min_poll_interval_sec: default_min_poll_interval_sec(), - max_poll_interval_sec: default_max_poll_interval_sec(), - throttle_on_battery: default_throttle_on_battery(), - log_level: default_log_level(), - stats_file_path: default_stats_file_path(), - } - } -} diff --git a/src/cpu.rs b/src/cpu.rs index 2d7a32d..d0985e9 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -14,7 +14,7 @@ fn read_u64(path: impl AsRef) -> anyhow::Result { let content = fs::read_to_string(path)?; - Ok(content.trim().parse::()?) + Ok(content.trim().parse()?) } fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { @@ -73,7 +73,7 @@ impl Cpu { }; // Has to match "cpu{N}". - let Ok(number) = cpu_prefix_removed.parse::() else { + let Ok(number) = cpu_prefix_removed.parse() else { continue; }; diff --git a/src/main.rs b/src/main.rs index 9902b79..68b929f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use anyhow::Context; use clap::Parser as _; use std::fmt::Write as _; use std::io::Write as _; +use std::path::PathBuf; use std::{io, process}; use yansi::Paint as _; @@ -29,57 +30,17 @@ enum Command { Info, /// Start the daemon. - Start, + Start { + /// The daemon config path. + #[arg(long, env = "SUPERFREQ_CONFIG")] + config: PathBuf, + }, /// 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(short = 'g', long)] - governor: Option, // TODO: Validate with clap for available governors. - - /// Set CPU Energy Performance Preference (EPP). Short form: --epp. - #[arg(short = 'p', long, alias = "epp")] - energy_performance_preference: Option, - - /// Set CPU Energy Performance Bias (EPB). Short form: --epb. - #[arg(short = 'b', 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(short = 't', long, conflicts_with = "for_")] - turbo: Option, - }, + CpuSet(config::CpuDelta), /// 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 = '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 = '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, - }, + PowerSet(config::PowerDelta), } fn real_main() -> anyhow::Result<()> { @@ -91,17 +52,17 @@ fn real_main() -> anyhow::Result<()> { .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(()) + Command::Start { config } => { + let config = config::DaemonConfig::load_from(&config) + .context("failed to load daemon config file")?; + + daemon::run(config) } - Command::CpuSet { + Command::CpuSet(config::CpuDelta { for_, governor, energy_performance_preference, @@ -109,7 +70,7 @@ fn real_main() -> anyhow::Result<()> { frequency_mhz_minimum, frequency_mhz_maximum, turbo, - } => { + }) => { let cpus = match for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); @@ -152,12 +113,12 @@ fn real_main() -> anyhow::Result<()> { Ok(()) } - Command::PowerSet { + Command::PowerSet(config::PowerDelta { for_, charge_threshold_start, charge_threshold_end, platform_profile, - } => { + }) => { let power_supplies = match for_ { Some(names) => { let mut power_supplies = Vec::with_capacity(names.len()); From 6349055c64d8ebc287d779af4164ba1c0f57844e Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:32:08 +0300 Subject: [PATCH 12/93] config: fix schema, toml does not have top level lists --- src/config.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0e07031..47de929 100644 --- a/src/config.rs +++ b/src/config.rs @@ -111,7 +111,9 @@ pub struct DaemonConfigLayer { #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[serde(transparent, default, rename_all = "kebab-case")] -pub struct DaemonConfig(pub Vec); +pub struct DaemonConfig { + config: Vec, +} impl DaemonConfig { pub fn load_from(path: &Path) -> anyhow::Result { @@ -122,9 +124,9 @@ impl DaemonConfig { let config: Self = toml::from_str(&contents).context("failed to parse config file")?; { - let mut priorities = Vec::with_capacity(config.0.len()); + let mut priorities = Vec::with_capacity(config.config.len()); - for layer in &config.0 { + for layer in &config.config { if priorities.contains(&layer.priority) { bail!("each config layer must have a different priority") } From 50168258024cacae3c49fca672b6ca6fa4bcbede Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:36:36 +0300 Subject: [PATCH 13/93] main: move application to deltas, comment out broken modules for now --- src/config.rs | 84 ++++++++++++++++++++++++++++++++++++++++ src/cpu.rs | 8 ++-- src/main.rs | 103 ++++---------------------------------------------- 3 files changed, 96 insertions(+), 99 deletions(-) diff --git a/src/config.rs b/src/config.rs index 47de929..cc9f18c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,8 @@ use std::{fs, path::Path}; use anyhow::{Context, bail}; use serde::{Deserialize, Serialize}; +use crate::{cpu, power_supply}; + fn is_default(value: &T) -> bool { *value == T::default() } @@ -46,6 +48,51 @@ pub struct CpuDelta { pub turbo: Option, } +impl CpuDelta { + pub fn apply(&self) -> anyhow::Result<()> { + let cpus = match &self.for_ { + 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) = self.governor.as_ref() { + cpu.set_governor(governor)?; + } + + if let Some(epp) = self.energy_performance_preference.as_ref() { + cpu.set_epp(epp)?; + } + + if let Some(epb) = self.energy_performance_bias.as_ref() { + cpu.set_epb(epb)?; + } + + if let Some(mhz_minimum) = self.frequency_mhz_minimum { + cpu.set_frequency_minimum(mhz_minimum)?; + } + + if let Some(mhz_maximum) = self.frequency_mhz_maximum { + cpu.set_frequency_maximum(mhz_maximum)?; + } + } + + if let Some(turbo) = self.turbo { + cpu::Cpu::set_turbo(turbo)?; + } + + Ok(()) + } +} + #[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")] pub struct PowerDelta { @@ -70,6 +117,43 @@ pub struct PowerDelta { pub platform_profile: Option, } +impl PowerDelta { + pub fn apply(&self) -> anyhow::Result<()> { + let power_supplies = match &self.for_ { + Some(names) => { + let mut power_supplies = Vec::with_capacity(names.len()); + + for name in names { + power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?); + } + + power_supplies + } + + None => power_supply::PowerSupply::all()? + .into_iter() + .filter(|power_supply| power_supply.threshold_config.is_some()) + .collect(), + }; + + for power_supply in power_supplies { + if let Some(threshold_start) = self.charge_threshold_start { + power_supply.set_charge_threshold_start(threshold_start)?; + } + + if let Some(threshold_end) = self.charge_threshold_end { + power_supply.set_charge_threshold_end(threshold_end)?; + } + } + + if let Some(platform_profile) = self.platform_profile.as_ref() { + power_supply::PowerSupply::set_platform_profile(platform_profile)?; + } + + Ok(()) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] #[serde(untagged, rename_all = "kebab-case")] pub enum Condition { diff --git a/src/cpu.rs b/src/cpu.rs index d0985e9..4d9d551 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -236,7 +236,7 @@ impl Cpu { 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_mhz * 1000; let frequency_khz = frequency_khz.to_string(); write( @@ -258,7 +258,7 @@ impl Cpu { return Ok(()); }; - if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz { + if new_frequency_mhz * 1000 < minimum_frequency_khz { bail!( "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}", minimum_frequency_khz / 1000, @@ -274,7 +274,7 @@ impl Cpu { 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_mhz * 1000; let frequency_khz = frequency_khz.to_string(); write( @@ -344,7 +344,7 @@ impl Cpu { let Cpu { number, .. } = cpu; write( - &format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), value_boost, ) .is_ok() diff --git a/src/main.rs b/src/main.rs index 68b929f..52f33c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ mod config; -mod core; +// mod core; mod cpu; -mod daemon; -mod engine; -mod monitor; +// mod daemon; +// mod engine; +// mod monitor; mod power_supply; use anyhow::Context; @@ -56,102 +56,15 @@ fn real_main() -> anyhow::Result<()> { Command::Info => todo!(), Command::Start { config } => { - let config = config::DaemonConfig::load_from(&config) + let _config = config::DaemonConfig::load_from(&config) .context("failed to load daemon config file")?; - daemon::run(config) - } - - Command::CpuSet(config::CpuDelta { - for_, - governor, - energy_performance_preference, - energy_performance_bias, - frequency_mhz_minimum, - frequency_mhz_maximum, - turbo, - }) => { - let cpus = match for_ { - 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)?; - } - - if let Some(epp) = energy_performance_preference.as_ref() { - cpu.set_epp(epp)?; - } - - if let Some(epb) = energy_performance_bias.as_ref() { - cpu.set_epb(epb)?; - } - - if let Some(mhz_minimum) = frequency_mhz_minimum { - cpu.set_frequency_minimum(mhz_minimum)?; - } - - if let Some(mhz_maximum) = frequency_mhz_maximum { - cpu.set_frequency_maximum(mhz_maximum)?; - } - } - - if let Some(turbo) = turbo { - cpu::Cpu::set_turbo(turbo)?; - } - + // daemon::run(config) Ok(()) } - Command::PowerSet(config::PowerDelta { - for_, - charge_threshold_start, - charge_threshold_end, - platform_profile, - }) => { - let power_supplies = match for_ { - Some(names) => { - let mut power_supplies = Vec::with_capacity(names.len()); - - for name in names { - power_supplies.push(power_supply::PowerSupply::from_name(name)?); - } - - power_supplies - } - - None => power_supply::PowerSupply::all()? - .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(threshold_start)?; - } - - if let Some(threshold_end) = charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end)?; - } - } - - if let Some(platform_profile) = platform_profile.as_ref() { - power_supply::PowerSupply::set_platform_profile(platform_profile); - } - - Ok(()) - } + Command::CpuSet(delta) => delta.apply(), + Command::PowerSet(delta) => delta.apply(), } } From 36e4bc05af67606d9876f844449c65fc2092b73c Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:44:27 +0300 Subject: [PATCH 14/93] power_supply&cpu: somewhat improve error messages --- src/config.rs | 2 +- src/cpu.rs | 11 +++++++---- src/power_supply.rs | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index cc9f18c..5d528b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,7 +60,7 @@ impl CpuDelta { cpus } - None => cpu::Cpu::all()?, + None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, }; for cpu in cpus { diff --git a/src/cpu.rs b/src/cpu.rs index 4d9d551..ceac01d 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -92,10 +92,13 @@ impl Cpu { /// 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, - )); + let Self { number, .. } = self; + + if !exists(format!("/sys/devices/system/cpu/cpu{number}")) { + bail!("{self} does not exist"); + } + + let has_cpufreq = exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq = has_cpufreq; diff --git a/src/power_supply.rs b/src/power_supply.rs index 9ec00ab..12649b8 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -62,12 +62,7 @@ pub struct PowerSupply { impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "power supply '{name}' at '{path}'", - name = &self.name, - path = self.path.display(), - )?; + write!(f, "power supply '{name}'", name = &self.name)?; if let Some(config) = self.threshold_config.as_ref() { write!( @@ -147,6 +142,10 @@ impl PowerSupply { } pub fn rescan(&mut self) -> anyhow::Result<()> { + if !self.path.exists() { + bail!("{self} does not exist"); + } + let threshold_config = self .get_type() .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? From 986e7e08b5cc566522dff8a4bf4d6a30bdca5796 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:47:56 +0300 Subject: [PATCH 15/93] power_supply&cpu: kolor --- src/cpu.rs | 4 +++- src/power_supply.rs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index ceac01d..17a5da1 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,4 +1,5 @@ use anyhow::{Context, bail}; +use yansi::Paint as _; use std::{fmt, fs, path::Path, string::ToString}; @@ -36,7 +37,8 @@ pub struct Cpu { impl fmt::Display for Cpu { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { number, .. } = self; + let number = self.number.cyan(); + write!(f, "CPU {number}") } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 12649b8..1f69a3c 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,4 +1,5 @@ use anyhow::{Context, bail}; +use yansi::Paint as _; use std::{ fmt, fs, @@ -62,13 +63,13 @@ 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}'", name = self.name.yellow())?; if let Some(config) = self.threshold_config.as_ref() { write!( f, " from manufacturer '{manufacturer}'", - manufacturer = config.manufacturer, + manufacturer = config.manufacturer.green(), )?; } From 923f759533336d2dcd508fb67765e1d66b3a27f8 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 22:22:26 +0300 Subject: [PATCH 16/93] config: better more enhanched expression --- src/config.rs | 108 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5d528b8..f8fb584 100644 --- a/src/config.rs +++ b/src/config.rs @@ -154,38 +154,103 @@ impl PowerDelta { } } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(untagged, rename_all = "kebab-case")] -pub enum Condition { - ChargeLessThan(u8), - ChargeMoreThan(u8), +pub enum Expression { + #[serde(rename = "$cpu-temperature")] + CpuTemperature, - TemperatureLessThan(u8), - TemperatureMoreThan(u8), + #[serde(rename = "%cpu-volatility")] + CpuVolatility, - UtilizationLessThan(u8), - UtilizationMoreThan(u8), + #[serde(rename = "%cpu-utilization")] + CpuUtilization, + #[serde(rename = "%power-supply-charge")] + PowerSupplyCharge, + + #[serde(rename = "%power-supply-discharge-rate")] + PowerSupplyDischargeRate, + + #[serde(rename = "?charging")] Charging, + #[serde(rename = "?on-battery")] OnBattery, + #[serde(rename = "#false")] False, + #[default] + #[serde(rename = "#true")] True, - All(Vec), - Any(Vec), + Number(f64), - Not(Box), + Plus { + value: Box, + plus: Box, + }, + Minus { + value: Box, + minus: Box, + }, + Multiply { + value: Box, + multiply: Box, + }, + Power { + value: Box, + power: Box, + }, + Divide { + value: Box, + divide: Box, + }, + + LessThan { + value: Box, + is_less_than: Box, + }, + + MoreThan { + value: Box, + is_more_than: Box, + }, + + Equal { + value: Box, + is_equal: Box, + leeway: Box, + }, + + And { + value: Box, + and: Box, + }, + All { + all: Vec, + }, + + Or { + value: Box, + or: Box, + }, + Any { + any: Vec, + }, + + Not { + not: Box, + }, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] -pub struct DaemonConfigLayer { +pub struct Rule { priority: u8, #[serde(default, skip_serializing_if = "is_default")] - if_: Condition, + if_: Expression, #[serde(default, skip_serializing_if = "is_default")] cpu: CpuDelta, @@ -193,10 +258,11 @@ pub struct DaemonConfigLayer { power: PowerDelta, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(transparent, default, rename_all = "kebab-case")] pub struct DaemonConfig { - config: Vec, + #[serde(rename = "rule")] + rules: Vec, } impl DaemonConfig { @@ -208,14 +274,14 @@ impl DaemonConfig { let config: Self = toml::from_str(&contents).context("failed to parse config file")?; { - let mut priorities = Vec::with_capacity(config.config.len()); + let mut priorities = Vec::with_capacity(config.rules.len()); - for layer in &config.config { - if priorities.contains(&layer.priority) { - bail!("each config layer must have a different priority") + for rule in &config.rules { + if priorities.contains(&rule.priority) { + bail!("each config rule must have a different priority") } - priorities.push(layer.priority); + priorities.push(rule.priority); } } From a5151f475bebbdea52fb9e9a0ad0e433e67579ab Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 22:32:54 +0300 Subject: [PATCH 17/93] daemon: wip new impl --- src/config.rs | 14 +- src/daemon.rs | 771 +++++++++++----------------------------------- src/daemon_old.rs | 649 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 +- 4 files changed, 834 insertions(+), 607 deletions(-) create mode 100644 src/daemon_old.rs diff --git a/src/config.rs b/src/config.rs index f8fb584..585b7a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -157,14 +157,20 @@ impl PowerDelta { #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(untagged, rename_all = "kebab-case")] pub enum Expression { + #[serde(rename = "%cpu-usage")] + CpuUsage, + + #[serde(rename = "$cpu-usage-volatility")] + CpuUsageVolatility, + #[serde(rename = "$cpu-temperature")] CpuTemperature, - #[serde(rename = "%cpu-volatility")] - CpuVolatility, + #[serde(rename = "$cpu-temperature-volatility")] + CpuTemperatureVolatility, - #[serde(rename = "%cpu-utilization")] - CpuUtilization, + #[serde(rename = "$cpu-idle-seconds")] + CpuIdleSeconds, #[serde(rename = "%power-supply-charge")] PowerSupplyCharge, diff --git a/src/daemon.rs b/src/daemon.rs index ba6d37d..55241a5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,649 +1,222 @@ -use anyhow::Context; -use anyhow::bail; +use std::{ + collections::VecDeque, + ops, + time::{Duration, Instant}, +}; -use crate::config::AppConfig; -use crate::core::SystemReport; -use crate::engine; -use crate::monitor; -use std::collections::VecDeque; -use std::fs::File; -use std::io::Write; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; +use crate::config; -/// Parameters for computing optimal polling interval -struct IntervalParams { - /// Base polling interval in seconds - base_interval: u64, - /// Minimum allowed polling interval in seconds - min_interval: u64, - /// Maximum allowed polling interval in seconds - max_interval: u64, - /// How rapidly CPU usage is changing - cpu_volatility: f32, - /// How rapidly temperature is changing - temp_volatility: f32, - /// Battery discharge rate in %/hour if available - battery_discharge_rate: Option, - /// Time since last detected user activity - last_user_activity: Duration, - /// Whether the system appears to be idle - is_system_idle: bool, - /// Whether the system is running on battery power - on_battery: bool, -} - -/// Calculate the idle time multiplier based on system idle duration +/// Calculate the idle time multiplier based on system idle time. /// -/// Returns a multiplier between 1.0 and 5.0 (capped): +/// Returns a multiplier between 1.0 and 5.0: /// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 /// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) -fn idle_multiplier(idle_secs: u64) -> f32 { - if idle_secs == 0 { - return 1.0; // No idle time, no multiplier effect - } - - let idle_factor = if idle_secs < 120 { - // Less than 2 minutes (0 to 119 seconds) +fn idle_multiplier(idle_for: Duration) -> f64 { + let factor = match idle_for.as_secs() < 120 { + // Less than 2 minutes. // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) - 1.0 + (idle_secs as f32) / 120.0 - } else { - // 2 minutes (120 seconds) or more - let idle_time_minutes = idle_secs / 60; + true => (idle_for.as_secs() as f64) / 120.0, + + // 2 minutes or more. // Logarithmic scaling: 1.0 + log2(minutes) - 1.0 + (idle_time_minutes as f32).log2().max(0.5) + false => { + let idle_minutes = idle_for.as_secs() as f64 / 60.0; + idle_minutes.log2() + } }; - // Cap the multiplier to avoid excessive intervals - idle_factor.min(5.0) // max factor of 5x + // Clamp the multiplier to avoid excessive intervals. + (1.0 + factor).clamp(1.0, 5.0) } -/// 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) -> anyhow::Result { - // Use the centralized validation function - validate_poll_intervals(params.min_interval, params.max_interval)?; - - // Start with base interval - let mut adjusted_interval = params.base_interval; - - // If we're on battery, we want to be more aggressive about saving power - if params.on_battery { - // Apply a multiplier based on battery discharge rate - if let Some(discharge_rate) = params.battery_discharge_rate { - if discharge_rate > 20.0 { - // High discharge rate - increase polling interval significantly (3x) - adjusted_interval = adjusted_interval.saturating_mul(3); - } else if discharge_rate > 10.0 { - // Moderate discharge - double polling interval (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } else { - // Low discharge rate - increase by 50% (multiply by 3/2) - adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); - } - } else { - // If we don't know discharge rate, use a conservative multiplier (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } - } - - // Adjust for system idleness - if params.is_system_idle { - let idle_time_seconds = params.last_user_activity.as_secs(); - - // Apply adjustment only if the system has been idle for a non-zero duration - if idle_time_seconds > 0 { - let idle_factor = idle_multiplier(idle_time_seconds); - - log::debug!( - "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", - idle_time_seconds, - (idle_time_seconds as f32 / 60.0).round(), - idle_factor - ); - - // Convert f32 multiplier to integer-safe math - // Multiply by a large number first, then divide to maintain precision - // Use 1000 as the scaling factor to preserve up to 3 decimal places - let scaling_factor = 1000; - let scaled_factor = (idle_factor * scaling_factor as f32) as u64; - adjusted_interval = adjusted_interval - .saturating_mul(scaled_factor) - .saturating_div(scaling_factor); - } - // If idle_time_seconds is 0, no factor is applied by this block - } - - // Adjust for CPU/temperature volatility - if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - // For division by 2 (halving the interval), we can safely use integer division - adjusted_interval = (adjusted_interval / 2).max(1); - } - - // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval - let min_safe_interval = params.min_interval.max(1); - let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); - - // Blend the new interval with the cached value if available - let blended_interval = if let Some(cached) = system_history.last_computed_interval { - // Use a weighted average: 70% previous value, 30% new value - // This smooths out drastic changes in polling frequency - const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% - const NEW_VALUE_WEIGHT: u128 = 3; // 30% - const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 - - // XXX: Use u128 arithmetic to avoid overflow with large interval values - let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT - + u128::from(new_interval) * NEW_VALUE_WEIGHT) - / TOTAL_WEIGHT; - - result as u64 - } else { - new_interval - }; - - // Blended result still needs to respect the configured bounds - // Again enforce minimum of 1 second regardless of params.min_interval - Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) -} - -/// Tracks historical system data for "advanced" adaptive polling -#[derive(Debug)] -struct SystemHistory { - /// Last several CPU usage measurements - cpu_usage_history: VecDeque, - /// Last several temperature readings - temperature_history: VecDeque, - /// Time of last detected user activity +struct Daemon { + /// Last time when there was user activity. last_user_activity: Instant, - /// Previous battery percentage (to calculate discharge rate) - last_battery_percentage: Option, - /// Timestamp of last battery reading - last_battery_timestamp: Option, - /// Battery discharge rate (%/hour) - battery_discharge_rate: Option, - /// Time spent in each system state - state_durations: std::collections::HashMap, - /// Last time a state transition happened - last_state_change: Instant, - /// Current system state - current_state: SystemState, - /// Last computed optimal polling interval - last_computed_interval: Option, + + /// CPU usage and temperature log. + cpu_log: VecDeque, + + /// Power supply status log. + power_supply_log: VecDeque, + + charging: bool, } -impl Default for SystemHistory { - fn default() -> Self { - Self { - cpu_usage_history: VecDeque::new(), - temperature_history: VecDeque::new(), - last_user_activity: Instant::now(), - last_battery_percentage: None, - last_battery_timestamp: None, - battery_discharge_rate: None, - state_durations: std::collections::HashMap::new(), - last_state_change: Instant::now(), - current_state: SystemState::default(), - last_computed_interval: None, - } - } +struct CpuLog { + at: Instant, + + /// CPU usage between 0-1, a percentage. + usage: f64, + + /// CPU temperature in celcius. + temperature: f64, } -impl SystemHistory { - /// Update system history with new report data - fn update(&mut self, report: &SystemReport) { - // Update CPU usage history - if !report.cpu_cores.is_empty() { - let mut total_usage: f32 = 0.0; - let mut core_count: usize = 0; +struct CpuVolatility { + at: ops::Range, - for core in &report.cpu_cores { - if let Some(usage) = core.usage_percent { - total_usage += usage; - core_count += 1; - } - } + usage: f64, - if core_count > 0 { - let avg_usage = total_usage / core_count as f32; + temperature: f64, +} - // Keep only the last 5 measurements - if self.cpu_usage_history.len() >= 5 { - self.cpu_usage_history.pop_front(); - } - self.cpu_usage_history.push_back(avg_usage); - - // Update last_user_activity if CPU usage indicates activity - // Consider significant CPU usage or sudden change as user activity - if avg_usage > 20.0 - || (self.cpu_usage_history.len() > 1 - && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) - .abs() - > 15.0) - { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on CPU usage"); - } - } +impl Daemon { + fn cpu_volatility(&self) -> Option { + if self.cpu_log.len() < 2 { + return None; } - // Update temperature history - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if self.temperature_history.len() >= 5 { - self.temperature_history.pop_front(); - } - self.temperature_history.push_back(temp); + let change_count = self.cpu_log.len() - 1; - // Significant temperature increase can indicate user activity - if self.temperature_history.len() > 1 { - let temp_change = - temp - self.temperature_history[self.temperature_history.len() - 2]; - if temp_change > 5.0 { - // 5°C rise in temperature - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on temperature change"); - } - } + let mut usage_change_sum = 0.0; + let mut temperature_change_sum = 0.0; + + for index in 0..change_count { + let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage; + usage_change_sum += usage_change.abs(); + + let temperature_change = + self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature; + temperature_change_sum += temperature_change.abs(); } - // Update battery discharge rate - if let Some(battery) = report.batteries.first() { - // Reset when we are charging or have just connected AC - if battery.ac_connected { - // Reset discharge tracking but continue updating the rest of - // the history so we still detect activity/load changes on AC. - self.battery_discharge_rate = None; - self.last_battery_percentage = None; - self.last_battery_timestamp = None; - } + Some(CpuVolatility { + at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at, - if let Some(current_percentage) = battery.capacity_percent { - let current_percent = f32::from(current_percentage); - - if let (Some(last_percentage), Some(last_timestamp)) = - (self.last_battery_percentage, self.last_battery_timestamp) - { - let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; - // Only calculate discharge rate if at least 30 seconds have passed - // and we're not on AC power - if elapsed_hours > 0.0083 && !battery.ac_connected { - // 0.0083 hours = 30 seconds - // Calculate discharge rate in percent per hour - let percent_change = last_percentage - current_percent; - if percent_change > 0.0 { - // Only if battery is discharging - let hourly_rate = percent_change / elapsed_hours; - // Clamp the discharge rate to a reasonable maximum value (100%/hour) - let clamped_rate = hourly_rate.min(100.0); - self.battery_discharge_rate = Some(clamped_rate); - } - } - } - - self.last_battery_percentage = Some(current_percent); - self.last_battery_timestamp = Some(Instant::now()); - } - } - - // Update system state tracking - let new_state = determine_system_state(report, self); - if new_state != self.current_state { - // Record time spent in previous state - let time_in_state = self.last_state_change.elapsed(); - *self - .state_durations - .entry(self.current_state.clone()) - .or_insert(Duration::ZERO) += time_in_state; - - // State changes (except to Idle) likely indicate user activity - if new_state != SystemState::Idle && new_state != SystemState::LowLoad { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system state change to {new_state:?}"); - } - - // Update state - self.current_state = new_state; - self.last_state_change = Instant::now(); - } - - // Check for significant load changes - if report.system_load.load_avg_1min > 1.0 { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system load"); - } + usage: usage_change_sum / change_count as f64, + temperature: temperature_change_sum / change_count as f64, + }) } - /// Calculate CPU usage volatility (how much it's changing) - fn get_cpu_volatility(&self) -> f32 { - if self.cpu_usage_history.len() < 2 { - return 0.0; - } + fn is_cpu_idle(&self) -> bool { + let recent_log_count = self + .cpu_log + .iter() + .rev() + .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) + .count(); - let mut sum_of_changes = 0.0; - for i in 1..self.cpu_usage_history.len() { - sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); - } - - sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 - } - - /// Calculate temperature volatility - fn get_temperature_volatility(&self) -> f32 { - if self.temperature_history.len() < 2 { - return 0.0; - } - - let mut sum_of_changes = 0.0; - for i in 1..self.temperature_history.len() { - sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); - } - - sum_of_changes / (self.temperature_history.len() - 1) as f32 - } - - /// Determine if the system appears to be idle - fn is_system_idle(&self) -> bool { - if self.cpu_usage_history.is_empty() { + if recent_log_count < 2 { return false; } - // System considered idle if the average CPU usage of last readings is below 10% - let recent_avg = - self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; - recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 - } + let recent_average = self + .cpu_log + .iter() + .rev() + .take(recent_log_count) + .map(|log| log.usage) + .sum::() + / recent_log_count as f64; - /// Calculate optimal polling interval based on system conditions - fn calculate_optimal_interval( - &self, - config: &AppConfig, - on_battery: bool, - ) -> anyhow::Result { - let params = IntervalParams { - base_interval: config.daemon.poll_interval_sec, - min_interval: config.daemon.min_poll_interval_sec, - max_interval: config.daemon.max_poll_interval_sec, - cpu_volatility: self.get_cpu_volatility(), - temp_volatility: self.get_temperature_volatility(), - battery_discharge_rate: self.battery_discharge_rate, - last_user_activity: self.last_user_activity.elapsed(), - is_system_idle: self.is_system_idle(), - on_battery, - }; - - compute_new(¶ms, self) + recent_average < 0.1 + && self + .cpu_volatility() + .is_none_or(|volatility| volatility.usage < 0.05) } } -/// 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) -> anyhow::Result<()> { - if min_interval < 1 { - bail!("min_interval must be ≥ 1"); - } - if max_interval < 1 { - bail!("max_interval must be ≥ 1"); - } - if max_interval >= min_interval { - Ok(()) - } else { - bail!( - "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" - ); +struct PowerSupplyLog { + at: Instant, + + /// Charge 0-1, as a percentage. + charge: f64, +} + +impl Daemon { + /// Calculates the discharge rate, returns a number between 0 and 1. + /// + /// The discharge rate is averaged per hour. + /// So a return value of Some(0.3) means the battery has been + /// discharging 30% per hour. + fn power_supply_discharge_rate(&self) -> Option { + let mut last_charge = None; + + // A list of increasing charge percentages. + let discharging: Vec<&PowerSupplyLog> = self + .power_supply_log + .iter() + .rev() + .take_while(move |log| { + let Some(last_charge_value) = last_charge else { + last_charge = Some(log.charge); + return true; + }; + + last_charge = Some(log.charge); + + log.charge > last_charge_value + }) + .collect(); + + if discharging.len() < 2 { + return None; + } + + // Start of discharging. Has the most charge. + let start = discharging.last().unwrap(); + // End of discharging, very close to now. Has the least charge. + let end = discharging.first().unwrap(); + + let discharging_duration_seconds = (start.at - end.at).as_secs_f64(); + let discharging_duration_hours = discharging_duration_seconds / 60.0 / 60.0; + let discharged = start.charge - end.charge; + + Some(discharged / discharging_duration_hours) } } -/// Run the daemon -pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { - log::info!("Starting superfreq daemon..."); +impl Daemon { + fn polling_interval(&self) -> Duration { + let mut interval = Duration::from_secs(5); - // Validate critical configuration values before proceeding - validate_poll_intervals( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - )?; - - // Create a flag that will be set to true when a signal is received - let running = Arc::new(AtomicBool::new(true)); - let r = running.clone(); - - // Set up signal handlers - ctrlc::set_handler(move || { - log::info!("Received shutdown signal, exiting..."); - r.store(false, Ordering::SeqCst); - }) - .context("failed to set Ctrl-C handler")?; - - 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 { - 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 { - log::warn!( - "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" - ); - } - let mut system_history = SystemHistory::default(); - - // Main loop - while running.load(Ordering::SeqCst) { - let start_time = Instant::now(); - - match monitor::collect_system_report(&config) { - Ok(report) => { - log::debug!("Collected system report, applying settings..."); - - // Store the current state before updating history - let previous_state = system_history.current_state.clone(); - - // Update system history with new data - system_history.update(&report); - - // 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) { - log::error!("Failed to write stats file: {e}"); - } - } - - match engine::determine_and_apply_settings(&report, &config, None) { - Ok(()) => { - log::debug!("Successfully applied system settings"); - - // If system state changed, log the new state - if system_history.current_state != previous_state { - log::info!( - "System state changed to: {:?}", - system_history.current_state - ); - } - } - Err(e) => { - log::error!("Error applying system settings: {e}"); - } - } - - // Check if we're on battery - let on_battery = !report.batteries.is_empty() - && report.batteries.first().is_some_and(|b| !b.ac_connected); - - // Calculate optimal polling interval if adaptive polling is enabled - if config.daemon.adaptive_interval { - match system_history.calculate_optimal_interval(&config, on_battery) { - Ok(optimal_interval) => { - // Store the new interval - system_history.last_computed_interval = Some(optimal_interval); - - log::debug!("Recalculated optimal interval: {optimal_interval}s"); - - // Don't change the interval too dramatically at once - match optimal_interval.cmp(¤t_poll_interval) { - std::cmp::Ordering::Greater => { - current_poll_interval = - (current_poll_interval + optimal_interval) / 2; - } - std::cmp::Ordering::Less => { - current_poll_interval = current_poll_interval - - ((current_poll_interval - optimal_interval) / 2).max(1); - } - std::cmp::Ordering::Equal => { - // No change needed when they're equal - } - } - } - Err(e) => { - // Log the error and stop the daemon when an invalid configuration is detected - log::error!("Critical configuration error: {e}"); - running.store(false, Ordering::SeqCst); - break; - } - } - - // Make sure that we respect the (user) configured min and max limits - current_poll_interval = current_poll_interval.clamp( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - ); - - 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 { - let battery_multiplier = 2; // poll half as often on battery - - // We need to make sure `poll_interval_sec` is *at least* 1 - // before multiplying. - let safe_interval = config.daemon.poll_interval_sec.max(1); - current_poll_interval = (safe_interval * battery_multiplier) - .min(config.daemon.max_poll_interval_sec); - - log::debug!( - "On battery power, increased poll interval to {current_poll_interval}s" - ); + // We are on battery, so we must be more conservative with our polling. + if !self.charging { + match self.power_supply_discharge_rate() { + Some(discharge_rate) => { + if discharge_rate > 0.2 { + interval *= 3; + } else if discharge_rate > 0.1 { + interval *= 2; } else { - // Use the configured poll interval - current_poll_interval = config.daemon.poll_interval_sec.max(1); - if config.daemon.poll_interval_sec == 0 { - log::debug!( - "Using minimum poll interval of 1s instead of configured 0s" - ); - } + // *= 1.5; + interval /= 2; + interval *= 3; } } - } - Err(e) => { - log::error!("Error collecting system report: {e}"); + + None => { + interval *= 2; + } } } - // Sleep for the remaining time in the poll interval - let elapsed = start_time.elapsed(); - let poll_duration = Duration::from_secs(current_poll_interval); - if elapsed < poll_duration { - let sleep_time = poll_duration - elapsed; - log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); - std::thread::sleep(sleep_time); + if self.is_cpu_idle() { + let idle_for = self.last_user_activity.elapsed(); + + if idle_for > Duration::from_secs(30) { + let factor = idle_multiplier(idle_for); + + log::debug!( + "system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x", + seconds = idle_for.as_secs(), + minutes = idle_for.as_secs() / 60, + ); + + interval = Duration::from_secs_f64(interval.as_secs_f64() * factor); + } } + + if let Some(volatility) = self.cpu_volatility() { + if volatility.usage > 0.1 || volatility.temperature > 0.02 { + interval = (interval / 2).max(Duration::from_secs(1)); + } + } + + todo!("implement rest from daemon_old.rs") } +} - log::info!("Daemon stopped"); +pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { Ok(()) } - -/// Write current system stats to a file for --stats to read -fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { - let mut file = File::create(path)?; - - writeln!(file, "timestamp={:?}", report.timestamp)?; - - // CPU info - writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; - writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; - if let Some(temp) = report.cpu_global.average_temperature_celsius { - writeln!(file, "cpu_temp={temp:.1}")?; - } - - // Battery info - if !report.batteries.is_empty() { - let battery = &report.batteries[0]; - writeln!(file, "ac_power={}", battery.ac_connected)?; - if let Some(cap) = battery.capacity_percent { - writeln!(file, "battery_percent={cap}")?; - } - } - - // System load - writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; - writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; - writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; - - Ok(()) -} - -/// Simplified system state used for determining when to adjust polling interval -#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] -enum SystemState { - #[default] - Unknown, - OnAC, - OnBattery, - HighLoad, - LowLoad, - HighTemp, - Idle, -} - -/// Determine the current system state for adaptive polling -fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { - // Check power state first - if !report.batteries.is_empty() { - if let Some(battery) = report.batteries.first() { - if battery.ac_connected { - return SystemState::OnAC; - } - return SystemState::OnBattery; - } - } - - // No batteries means desktop, so always AC - if report.batteries.is_empty() { - return SystemState::OnAC; - } - - // Check temperature - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if temp > 80.0 { - return SystemState::HighTemp; - } - } - - // Check load first, as high load should take precedence over idle state - let avg_load = report.system_load.load_avg_1min; - if avg_load > 3.0 { - return SystemState::HighLoad; - } - - // Check idle state only if we don't have high load - if history.is_system_idle() { - return SystemState::Idle; - } - - // Check for low load - if avg_load < 0.5 { - return SystemState::LowLoad; - } - - // Default case - SystemState::Unknown -} diff --git a/src/daemon_old.rs b/src/daemon_old.rs new file mode 100644 index 0000000..ba6d37d --- /dev/null +++ b/src/daemon_old.rs @@ -0,0 +1,649 @@ +use anyhow::Context; +use anyhow::bail; + +use crate::config::AppConfig; +use crate::core::SystemReport; +use crate::engine; +use crate::monitor; +use std::collections::VecDeque; +use std::fs::File; +use std::io::Write; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +/// Parameters for computing optimal polling interval +struct IntervalParams { + /// Base polling interval in seconds + base_interval: u64, + /// Minimum allowed polling interval in seconds + min_interval: u64, + /// Maximum allowed polling interval in seconds + max_interval: u64, + /// How rapidly CPU usage is changing + cpu_volatility: f32, + /// How rapidly temperature is changing + temp_volatility: f32, + /// Battery discharge rate in %/hour if available + battery_discharge_rate: Option, + /// Time since last detected user activity + last_user_activity: Duration, + /// Whether the system appears to be idle + is_system_idle: bool, + /// Whether the system is running on battery power + on_battery: bool, +} + +/// Calculate the idle time multiplier based on system idle duration +/// +/// Returns a multiplier between 1.0 and 5.0 (capped): +/// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 +/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) +fn idle_multiplier(idle_secs: u64) -> f32 { + if idle_secs == 0 { + return 1.0; // No idle time, no multiplier effect + } + + let idle_factor = if idle_secs < 120 { + // Less than 2 minutes (0 to 119 seconds) + // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) + 1.0 + (idle_secs as f32) / 120.0 + } else { + // 2 minutes (120 seconds) or more + let idle_time_minutes = idle_secs / 60; + // Logarithmic scaling: 1.0 + log2(minutes) + 1.0 + (idle_time_minutes as f32).log2().max(0.5) + }; + + // Cap the multiplier to avoid excessive intervals + idle_factor.min(5.0) // max factor of 5x +} + +/// 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) -> anyhow::Result { + // Use the centralized validation function + validate_poll_intervals(params.min_interval, params.max_interval)?; + + // Start with base interval + let mut adjusted_interval = params.base_interval; + + // If we're on battery, we want to be more aggressive about saving power + if params.on_battery { + // Apply a multiplier based on battery discharge rate + if let Some(discharge_rate) = params.battery_discharge_rate { + if discharge_rate > 20.0 { + // High discharge rate - increase polling interval significantly (3x) + adjusted_interval = adjusted_interval.saturating_mul(3); + } else if discharge_rate > 10.0 { + // Moderate discharge - double polling interval (2x) + adjusted_interval = adjusted_interval.saturating_mul(2); + } else { + // Low discharge rate - increase by 50% (multiply by 3/2) + adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); + } + } else { + // If we don't know discharge rate, use a conservative multiplier (2x) + adjusted_interval = adjusted_interval.saturating_mul(2); + } + } + + // Adjust for system idleness + if params.is_system_idle { + let idle_time_seconds = params.last_user_activity.as_secs(); + + // Apply adjustment only if the system has been idle for a non-zero duration + if idle_time_seconds > 0 { + let idle_factor = idle_multiplier(idle_time_seconds); + + log::debug!( + "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", + idle_time_seconds, + (idle_time_seconds as f32 / 60.0).round(), + idle_factor + ); + + // Convert f32 multiplier to integer-safe math + // Multiply by a large number first, then divide to maintain precision + // Use 1000 as the scaling factor to preserve up to 3 decimal places + let scaling_factor = 1000; + let scaled_factor = (idle_factor * scaling_factor as f32) as u64; + adjusted_interval = adjusted_interval + .saturating_mul(scaled_factor) + .saturating_div(scaling_factor); + } + // If idle_time_seconds is 0, no factor is applied by this block + } + + // Adjust for CPU/temperature volatility + if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { + // For division by 2 (halving the interval), we can safely use integer division + adjusted_interval = (adjusted_interval / 2).max(1); + } + + // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval + let min_safe_interval = params.min_interval.max(1); + let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); + + // Blend the new interval with the cached value if available + let blended_interval = if let Some(cached) = system_history.last_computed_interval { + // Use a weighted average: 70% previous value, 30% new value + // This smooths out drastic changes in polling frequency + const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% + const NEW_VALUE_WEIGHT: u128 = 3; // 30% + const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 + + // XXX: Use u128 arithmetic to avoid overflow with large interval values + let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT + + u128::from(new_interval) * NEW_VALUE_WEIGHT) + / TOTAL_WEIGHT; + + result as u64 + } else { + new_interval + }; + + // Blended result still needs to respect the configured bounds + // Again enforce minimum of 1 second regardless of params.min_interval + Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) +} + +/// Tracks historical system data for "advanced" adaptive polling +#[derive(Debug)] +struct SystemHistory { + /// Last several CPU usage measurements + cpu_usage_history: VecDeque, + /// Last several temperature readings + temperature_history: VecDeque, + /// Time of last detected user activity + last_user_activity: Instant, + /// Previous battery percentage (to calculate discharge rate) + last_battery_percentage: Option, + /// Timestamp of last battery reading + last_battery_timestamp: Option, + /// Battery discharge rate (%/hour) + battery_discharge_rate: Option, + /// Time spent in each system state + state_durations: std::collections::HashMap, + /// Last time a state transition happened + last_state_change: Instant, + /// Current system state + current_state: SystemState, + /// Last computed optimal polling interval + last_computed_interval: Option, +} + +impl Default for SystemHistory { + fn default() -> Self { + Self { + cpu_usage_history: VecDeque::new(), + temperature_history: VecDeque::new(), + last_user_activity: Instant::now(), + last_battery_percentage: None, + last_battery_timestamp: None, + battery_discharge_rate: None, + state_durations: std::collections::HashMap::new(), + last_state_change: Instant::now(), + current_state: SystemState::default(), + last_computed_interval: None, + } + } +} + +impl SystemHistory { + /// Update system history with new report data + fn update(&mut self, report: &SystemReport) { + // Update CPU usage history + if !report.cpu_cores.is_empty() { + let mut total_usage: f32 = 0.0; + let mut core_count: usize = 0; + + for core in &report.cpu_cores { + if let Some(usage) = core.usage_percent { + total_usage += usage; + core_count += 1; + } + } + + if core_count > 0 { + let avg_usage = total_usage / core_count as f32; + + // Keep only the last 5 measurements + if self.cpu_usage_history.len() >= 5 { + self.cpu_usage_history.pop_front(); + } + self.cpu_usage_history.push_back(avg_usage); + + // Update last_user_activity if CPU usage indicates activity + // Consider significant CPU usage or sudden change as user activity + if avg_usage > 20.0 + || (self.cpu_usage_history.len() > 1 + && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) + .abs() + > 15.0) + { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on CPU usage"); + } + } + } + + // Update temperature history + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if self.temperature_history.len() >= 5 { + self.temperature_history.pop_front(); + } + self.temperature_history.push_back(temp); + + // Significant temperature increase can indicate user activity + if self.temperature_history.len() > 1 { + let temp_change = + temp - self.temperature_history[self.temperature_history.len() - 2]; + if temp_change > 5.0 { + // 5°C rise in temperature + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on temperature change"); + } + } + } + + // Update battery discharge rate + if let Some(battery) = report.batteries.first() { + // Reset when we are charging or have just connected AC + if battery.ac_connected { + // Reset discharge tracking but continue updating the rest of + // the history so we still detect activity/load changes on AC. + self.battery_discharge_rate = None; + self.last_battery_percentage = None; + self.last_battery_timestamp = None; + } + + if let Some(current_percentage) = battery.capacity_percent { + let current_percent = f32::from(current_percentage); + + if let (Some(last_percentage), Some(last_timestamp)) = + (self.last_battery_percentage, self.last_battery_timestamp) + { + let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; + // Only calculate discharge rate if at least 30 seconds have passed + // and we're not on AC power + if elapsed_hours > 0.0083 && !battery.ac_connected { + // 0.0083 hours = 30 seconds + // Calculate discharge rate in percent per hour + let percent_change = last_percentage - current_percent; + if percent_change > 0.0 { + // Only if battery is discharging + let hourly_rate = percent_change / elapsed_hours; + // Clamp the discharge rate to a reasonable maximum value (100%/hour) + let clamped_rate = hourly_rate.min(100.0); + self.battery_discharge_rate = Some(clamped_rate); + } + } + } + + self.last_battery_percentage = Some(current_percent); + self.last_battery_timestamp = Some(Instant::now()); + } + } + + // Update system state tracking + let new_state = determine_system_state(report, self); + if new_state != self.current_state { + // Record time spent in previous state + let time_in_state = self.last_state_change.elapsed(); + *self + .state_durations + .entry(self.current_state.clone()) + .or_insert(Duration::ZERO) += time_in_state; + + // State changes (except to Idle) likely indicate user activity + if new_state != SystemState::Idle && new_state != SystemState::LowLoad { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on system state change to {new_state:?}"); + } + + // Update state + self.current_state = new_state; + self.last_state_change = Instant::now(); + } + + // Check for significant load changes + if report.system_load.load_avg_1min > 1.0 { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on system load"); + } + } + + /// Calculate CPU usage volatility (how much it's changing) + fn get_cpu_volatility(&self) -> f32 { + if self.cpu_usage_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.cpu_usage_history.len() { + sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); + } + + sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 + } + + /// Calculate temperature volatility + fn get_temperature_volatility(&self) -> f32 { + if self.temperature_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.temperature_history.len() { + sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); + } + + sum_of_changes / (self.temperature_history.len() - 1) as f32 + } + + /// Determine if the system appears to be idle + fn is_system_idle(&self) -> bool { + if self.cpu_usage_history.is_empty() { + return false; + } + + // System considered idle if the average CPU usage of last readings is below 10% + let recent_avg = + self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; + recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 + } + + /// Calculate optimal polling interval based on system conditions + fn calculate_optimal_interval( + &self, + config: &AppConfig, + on_battery: bool, + ) -> anyhow::Result { + let params = IntervalParams { + base_interval: config.daemon.poll_interval_sec, + min_interval: config.daemon.min_poll_interval_sec, + max_interval: config.daemon.max_poll_interval_sec, + cpu_volatility: self.get_cpu_volatility(), + temp_volatility: self.get_temperature_volatility(), + battery_discharge_rate: self.battery_discharge_rate, + last_user_activity: self.last_user_activity.elapsed(), + is_system_idle: self.is_system_idle(), + on_battery, + }; + + compute_new(¶ms, self) + } +} + +/// 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) -> anyhow::Result<()> { + if min_interval < 1 { + bail!("min_interval must be ≥ 1"); + } + if max_interval < 1 { + bail!("max_interval must be ≥ 1"); + } + if max_interval >= min_interval { + Ok(()) + } else { + bail!( + "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" + ); + } +} + +/// Run the daemon +pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { + log::info!("Starting superfreq daemon..."); + + // Validate critical configuration values before proceeding + validate_poll_intervals( + config.daemon.min_poll_interval_sec, + config.daemon.max_poll_interval_sec, + )?; + + // Create a flag that will be set to true when a signal is received + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + // Set up signal handlers + ctrlc::set_handler(move || { + log::info!("Received shutdown signal, exiting..."); + r.store(false, Ordering::SeqCst); + }) + .context("failed to set Ctrl-C handler")?; + + 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 { + 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 { + log::warn!( + "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" + ); + } + let mut system_history = SystemHistory::default(); + + // Main loop + while running.load(Ordering::SeqCst) { + let start_time = Instant::now(); + + match monitor::collect_system_report(&config) { + Ok(report) => { + log::debug!("Collected system report, applying settings..."); + + // Store the current state before updating history + let previous_state = system_history.current_state.clone(); + + // Update system history with new data + system_history.update(&report); + + // 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) { + log::error!("Failed to write stats file: {e}"); + } + } + + match engine::determine_and_apply_settings(&report, &config, None) { + Ok(()) => { + log::debug!("Successfully applied system settings"); + + // If system state changed, log the new state + if system_history.current_state != previous_state { + log::info!( + "System state changed to: {:?}", + system_history.current_state + ); + } + } + Err(e) => { + log::error!("Error applying system settings: {e}"); + } + } + + // Check if we're on battery + let on_battery = !report.batteries.is_empty() + && report.batteries.first().is_some_and(|b| !b.ac_connected); + + // Calculate optimal polling interval if adaptive polling is enabled + if config.daemon.adaptive_interval { + match system_history.calculate_optimal_interval(&config, on_battery) { + Ok(optimal_interval) => { + // Store the new interval + system_history.last_computed_interval = Some(optimal_interval); + + log::debug!("Recalculated optimal interval: {optimal_interval}s"); + + // Don't change the interval too dramatically at once + match optimal_interval.cmp(¤t_poll_interval) { + std::cmp::Ordering::Greater => { + current_poll_interval = + (current_poll_interval + optimal_interval) / 2; + } + std::cmp::Ordering::Less => { + current_poll_interval = current_poll_interval + - ((current_poll_interval - optimal_interval) / 2).max(1); + } + std::cmp::Ordering::Equal => { + // No change needed when they're equal + } + } + } + Err(e) => { + // Log the error and stop the daemon when an invalid configuration is detected + log::error!("Critical configuration error: {e}"); + running.store(false, Ordering::SeqCst); + break; + } + } + + // Make sure that we respect the (user) configured min and max limits + current_poll_interval = current_poll_interval.clamp( + config.daemon.min_poll_interval_sec, + config.daemon.max_poll_interval_sec, + ); + + 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 { + let battery_multiplier = 2; // poll half as often on battery + + // We need to make sure `poll_interval_sec` is *at least* 1 + // before multiplying. + let safe_interval = config.daemon.poll_interval_sec.max(1); + current_poll_interval = (safe_interval * battery_multiplier) + .min(config.daemon.max_poll_interval_sec); + + 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 { + log::debug!( + "Using minimum poll interval of 1s instead of configured 0s" + ); + } + } + } + } + Err(e) => { + log::error!("Error collecting system report: {e}"); + } + } + + // Sleep for the remaining time in the poll interval + let elapsed = start_time.elapsed(); + let poll_duration = Duration::from_secs(current_poll_interval); + if elapsed < poll_duration { + let sleep_time = poll_duration - elapsed; + log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); + std::thread::sleep(sleep_time); + } + } + + log::info!("Daemon stopped"); + Ok(()) +} + +/// Write current system stats to a file for --stats to read +fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { + let mut file = File::create(path)?; + + writeln!(file, "timestamp={:?}", report.timestamp)?; + + // CPU info + writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; + writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; + if let Some(temp) = report.cpu_global.average_temperature_celsius { + writeln!(file, "cpu_temp={temp:.1}")?; + } + + // Battery info + if !report.batteries.is_empty() { + let battery = &report.batteries[0]; + writeln!(file, "ac_power={}", battery.ac_connected)?; + if let Some(cap) = battery.capacity_percent { + writeln!(file, "battery_percent={cap}")?; + } + } + + // System load + writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; + writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; + writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; + + Ok(()) +} + +/// Simplified system state used for determining when to adjust polling interval +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] +enum SystemState { + #[default] + Unknown, + OnAC, + OnBattery, + HighLoad, + LowLoad, + HighTemp, + Idle, +} + +/// Determine the current system state for adaptive polling +fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { + // Check power state first + if !report.batteries.is_empty() { + if let Some(battery) = report.batteries.first() { + if battery.ac_connected { + return SystemState::OnAC; + } + return SystemState::OnBattery; + } + } + + // No batteries means desktop, so always AC + if report.batteries.is_empty() { + return SystemState::OnAC; + } + + // Check temperature + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if temp > 80.0 { + return SystemState::HighTemp; + } + } + + // Check load first, as high load should take precedence over idle state + let avg_load = report.system_load.load_avg_1min; + if avg_load > 3.0 { + return SystemState::HighLoad; + } + + // Check idle state only if we don't have high load + if history.is_system_idle() { + return SystemState::Idle; + } + + // Check for low load + if avg_load < 0.5 { + return SystemState::LowLoad; + } + + // Default case + SystemState::Unknown +} diff --git a/src/main.rs b/src/main.rs index 52f33c9..0725e38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod config; // mod core; mod cpu; -// mod daemon; +mod daemon; // mod engine; // mod monitor; mod power_supply; @@ -56,11 +56,10 @@ fn real_main() -> anyhow::Result<()> { Command::Info => todo!(), Command::Start { config } => { - let _config = config::DaemonConfig::load_from(&config) + let config = config::DaemonConfig::load_from(&config) .context("failed to load daemon config file")?; - // daemon::run(config) - Ok(()) + daemon::run(config) } Command::CpuSet(delta) => delta.apply(), From a862b0b45601cad1b44cfd00beaf76a6f3d0b6e4 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 18:41:02 +0300 Subject: [PATCH 18/93] daemon: implement polling_interval --- src/daemon.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 55241a5..6275426 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -39,7 +39,11 @@ struct Daemon { /// Power supply status log. power_supply_log: VecDeque, + /// Whether if we are charging right now. charging: bool, + + /// The last computed polling interval. + last_polling_interval: Option, } struct CpuLog { @@ -167,7 +171,7 @@ impl Daemon { } impl Daemon { - fn polling_interval(&self) -> Duration { + fn polling_interval(&mut self) -> Duration { let mut interval = Duration::from_secs(5); // We are on battery, so we must be more conservative with our polling. @@ -185,6 +189,8 @@ impl Daemon { } } + // If we can't deterine the discharge rate, that means that + // we were very recently started. Which is user activity. None => { interval *= 2; } @@ -213,7 +219,20 @@ impl Daemon { } } - todo!("implement rest from daemon_old.rs") + let interval = match self.last_polling_interval { + Some(last_interval) => Duration::from_secs_f64( + // 30% of current computed interval, 70% of last interval. + interval.as_secs_f64() * 0.3 + last_interval.as_secs_f64() * 0.7, + ), + + None => interval, + }; + + let interval = Duration::from_secs_f64(interval.as_secs_f64().clamp(1.0, 30.0)); + + self.last_polling_interval = Some(interval); + + interval } } From 3a4b5fb530823d610153325f255d5363794a3862 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 18:55:06 +0300 Subject: [PATCH 19/93] daemon: delete some old code and create daemon scaffold --- config.toml | 2 + src/config.rs | 4 +- src/daemon.rs | 33 +++++-- src/daemon_old.rs | 223 ---------------------------------------------- 4 files changed, 31 insertions(+), 231 deletions(-) create mode 100644 config.toml diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..2f796b7 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +[[rule]] +priority = 0 diff --git a/src/config.rs b/src/config.rs index 585b7a7..9ca0498 100644 --- a/src/config.rs +++ b/src/config.rs @@ -255,7 +255,7 @@ pub enum Expression { pub struct Rule { priority: u8, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(default, rename = "if", skip_serializing_if = "is_default")] if_: Expression, #[serde(default, skip_serializing_if = "is_default")] @@ -265,7 +265,7 @@ pub struct Rule { } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] -#[serde(transparent, default, rename_all = "kebab-case")] +#[serde(default, rename_all = "kebab-case")] pub struct DaemonConfig { #[serde(rename = "rule")] rules: Vec, diff --git a/src/daemon.rs b/src/daemon.rs index 6275426..f2d2e3a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,9 +1,15 @@ use std::{ collections::VecDeque, ops, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, time::{Duration, Instant}, }; +use anyhow::Context; + use crate::config; /// Calculate the idle time multiplier based on system idle time. @@ -33,17 +39,17 @@ struct Daemon { /// Last time when there was user activity. last_user_activity: Instant, + /// The last computed polling interval. + last_polling_interval: Option, + + /// Whether if we are charging right now. + charging: bool, + /// CPU usage and temperature log. cpu_log: VecDeque, /// Power supply status log. power_supply_log: VecDeque, - - /// Whether if we are charging right now. - charging: bool, - - /// The last computed polling interval. - last_polling_interval: Option, } struct CpuLog { @@ -237,5 +243,20 @@ impl Daemon { } pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { + log::info!("starting daemon..."); + + let cancelled = Arc::new(AtomicBool::new(false)); + + let cancelled_ = Arc::clone(&cancelled); + ctrlc::set_handler(move || { + log::info!("received shutdown signal"); + cancelled_.store(true, Ordering::SeqCst); + }) + .context("failed to set Ctrl-C handler")?; + + while !cancelled.load(Ordering::SeqCst) {} + + log::info!("exiting..."); + Ok(()) } diff --git a/src/daemon_old.rs b/src/daemon_old.rs index ba6d37d..3a20cb4 100644 --- a/src/daemon_old.rs +++ b/src/daemon_old.rs @@ -12,143 +12,6 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -/// Parameters for computing optimal polling interval -struct IntervalParams { - /// Base polling interval in seconds - base_interval: u64, - /// Minimum allowed polling interval in seconds - min_interval: u64, - /// Maximum allowed polling interval in seconds - max_interval: u64, - /// How rapidly CPU usage is changing - cpu_volatility: f32, - /// How rapidly temperature is changing - temp_volatility: f32, - /// Battery discharge rate in %/hour if available - battery_discharge_rate: Option, - /// Time since last detected user activity - last_user_activity: Duration, - /// Whether the system appears to be idle - is_system_idle: bool, - /// Whether the system is running on battery power - on_battery: bool, -} - -/// Calculate the idle time multiplier based on system idle duration -/// -/// Returns a multiplier between 1.0 and 5.0 (capped): -/// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 -/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) -fn idle_multiplier(idle_secs: u64) -> f32 { - if idle_secs == 0 { - return 1.0; // No idle time, no multiplier effect - } - - let idle_factor = if idle_secs < 120 { - // Less than 2 minutes (0 to 119 seconds) - // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) - 1.0 + (idle_secs as f32) / 120.0 - } else { - // 2 minutes (120 seconds) or more - let idle_time_minutes = idle_secs / 60; - // Logarithmic scaling: 1.0 + log2(minutes) - 1.0 + (idle_time_minutes as f32).log2().max(0.5) - }; - - // Cap the multiplier to avoid excessive intervals - idle_factor.min(5.0) // max factor of 5x -} - -/// 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) -> anyhow::Result { - // Use the centralized validation function - validate_poll_intervals(params.min_interval, params.max_interval)?; - - // Start with base interval - let mut adjusted_interval = params.base_interval; - - // If we're on battery, we want to be more aggressive about saving power - if params.on_battery { - // Apply a multiplier based on battery discharge rate - if let Some(discharge_rate) = params.battery_discharge_rate { - if discharge_rate > 20.0 { - // High discharge rate - increase polling interval significantly (3x) - adjusted_interval = adjusted_interval.saturating_mul(3); - } else if discharge_rate > 10.0 { - // Moderate discharge - double polling interval (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } else { - // Low discharge rate - increase by 50% (multiply by 3/2) - adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); - } - } else { - // If we don't know discharge rate, use a conservative multiplier (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } - } - - // Adjust for system idleness - if params.is_system_idle { - let idle_time_seconds = params.last_user_activity.as_secs(); - - // Apply adjustment only if the system has been idle for a non-zero duration - if idle_time_seconds > 0 { - let idle_factor = idle_multiplier(idle_time_seconds); - - log::debug!( - "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", - idle_time_seconds, - (idle_time_seconds as f32 / 60.0).round(), - idle_factor - ); - - // Convert f32 multiplier to integer-safe math - // Multiply by a large number first, then divide to maintain precision - // Use 1000 as the scaling factor to preserve up to 3 decimal places - let scaling_factor = 1000; - let scaled_factor = (idle_factor * scaling_factor as f32) as u64; - adjusted_interval = adjusted_interval - .saturating_mul(scaled_factor) - .saturating_div(scaling_factor); - } - // If idle_time_seconds is 0, no factor is applied by this block - } - - // Adjust for CPU/temperature volatility - if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - // For division by 2 (halving the interval), we can safely use integer division - adjusted_interval = (adjusted_interval / 2).max(1); - } - - // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval - let min_safe_interval = params.min_interval.max(1); - let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); - - // Blend the new interval with the cached value if available - let blended_interval = if let Some(cached) = system_history.last_computed_interval { - // Use a weighted average: 70% previous value, 30% new value - // This smooths out drastic changes in polling frequency - const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% - const NEW_VALUE_WEIGHT: u128 = 3; // 30% - const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 - - // XXX: Use u128 arithmetic to avoid overflow with large interval values - let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT - + u128::from(new_interval) * NEW_VALUE_WEIGHT) - / TOTAL_WEIGHT; - - result as u64 - } else { - new_interval - }; - - // Blended result still needs to respect the configured bounds - // Again enforce minimum of 1 second regardless of params.min_interval - Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) -} - /// Tracks historical system data for "advanced" adaptive polling #[derive(Debug)] struct SystemHistory { @@ -174,23 +37,6 @@ struct SystemHistory { last_computed_interval: Option, } -impl Default for SystemHistory { - fn default() -> Self { - Self { - cpu_usage_history: VecDeque::new(), - temperature_history: VecDeque::new(), - last_user_activity: Instant::now(), - last_battery_percentage: None, - last_battery_timestamp: None, - battery_discharge_rate: None, - state_durations: std::collections::HashMap::new(), - last_state_change: Instant::now(), - current_state: SystemState::default(), - last_computed_interval: None, - } - } -} - impl SystemHistory { /// Update system history with new report data fn update(&mut self, report: &SystemReport) { @@ -354,45 +200,6 @@ impl SystemHistory { self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 } - - /// Calculate optimal polling interval based on system conditions - fn calculate_optimal_interval( - &self, - config: &AppConfig, - on_battery: bool, - ) -> anyhow::Result { - let params = IntervalParams { - base_interval: config.daemon.poll_interval_sec, - min_interval: config.daemon.min_poll_interval_sec, - max_interval: config.daemon.max_poll_interval_sec, - cpu_volatility: self.get_cpu_volatility(), - temp_volatility: self.get_temperature_volatility(), - battery_discharge_rate: self.battery_discharge_rate, - last_user_activity: self.last_user_activity.elapsed(), - is_system_idle: self.is_system_idle(), - on_battery, - }; - - compute_new(¶ms, self) - } -} - -/// 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) -> anyhow::Result<()> { - if min_interval < 1 { - bail!("min_interval must be ≥ 1"); - } - if max_interval < 1 { - bail!("max_interval must be ≥ 1"); - } - if max_interval >= min_interval { - Ok(()) - } else { - bail!( - "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" - ); - } } /// Run the daemon @@ -561,36 +368,6 @@ pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { Ok(()) } -/// Write current system stats to a file for --stats to read -fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { - let mut file = File::create(path)?; - - writeln!(file, "timestamp={:?}", report.timestamp)?; - - // CPU info - writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; - writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; - if let Some(temp) = report.cpu_global.average_temperature_celsius { - writeln!(file, "cpu_temp={temp:.1}")?; - } - - // Battery info - if !report.batteries.is_empty() { - let battery = &report.batteries[0]; - writeln!(file, "ac_power={}", battery.ac_connected)?; - if let Some(cap) = battery.capacity_percent { - writeln!(file, "battery_percent={cap}")?; - } - } - - // System load - writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; - writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; - writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; - - Ok(()) -} - /// Simplified system state used for determining when to adjust polling interval #[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] enum SystemState { From 67e211558863c578c32e720c0658117bb74dfef6 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:10:10 +0300 Subject: [PATCH 20/93] cpu&power: share fs impls --- src/cpu.rs | 60 ++++++++++++++------------------------------- src/fs.rs | 55 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 ++++-- src/power_supply.rs | 32 +++++++++--------------- src/system.rs | 14 +++++++++++ 5 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 src/fs.rs create mode 100644 src/system.rs diff --git a/src/cpu.rs b/src/cpu.rs index 17a5da1..7b8ef99 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,33 +1,9 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{fmt, fs, path::Path, string::ToString}; +use std::{fmt, string::ToString}; -fn exists(path: impl AsRef) -> bool { - let path = path.as_ref(); - - path.exists() -} - -// 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(); - - 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(), - ) - }) -} +use crate::fs; #[derive(Debug, Clone, Copy)] pub struct Cpu { @@ -96,11 +72,11 @@ impl Cpu { pub fn rescan(&mut self) -> anyhow::Result<()> { let Self { number, .. } = self; - if !exists(format!("/sys/devices/system/cpu/cpu{number}")) { + if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { bail!("{self} does not exist"); } - let has_cpufreq = exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); + let has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq = has_cpufreq; @@ -110,7 +86,7 @@ impl Cpu { pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; - let Ok(content) = fs::read_to_string(format!( + let Ok(Some(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" )) else { return Vec::new(); @@ -137,7 +113,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), governor, ) @@ -151,7 +127,7 @@ impl Cpu { pub fn get_available_epps(&self) -> Vec { let Self { number, .. } = self; - let Ok(content) = fs::read_to_string(format!( + let Ok(Some(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" )) else { return Vec::new(); @@ -175,7 +151,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), epp, ) @@ -226,7 +202,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), epb, ) @@ -244,7 +220,7 @@ impl Cpu { let frequency_khz = frequency_mhz * 1000; let frequency_khz = frequency_khz.to_string(); - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"), &frequency_khz, ) @@ -256,7 +232,7 @@ impl Cpu { fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(minimum_frequency_khz) = read_u64(format!( + let Ok(minimum_frequency_khz) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -282,7 +258,7 @@ impl Cpu { let frequency_khz = frequency_mhz * 1000; let frequency_khz = frequency_khz.to_string(); - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"), &frequency_khz, ) @@ -294,7 +270,7 @@ impl Cpu { fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(maximum_frequency_khz) = read_u64(format!( + let Ok(maximum_frequency_khz) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -331,16 +307,16 @@ impl Cpu { 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() { + if fs::write(intel_boost_path_negated, value_boost_negated).is_ok() { return Ok(()); } - if write(amd_boost_path, value_boost).is_ok() { + if fs::write(amd_boost_path, value_boost).is_ok() { return Ok(()); } - if write(msr_boost_path, value_boost).is_ok() { + if fs::write(msr_boost_path, value_boost).is_ok() { return Ok(()); } - if write(generic_boost_path, value_boost).is_ok() { + if fs::write(generic_boost_path, value_boost).is_ok() { return Ok(()); } @@ -348,7 +324,7 @@ impl Cpu { if Self::all()?.iter().any(|cpu| { let Cpu { number, .. } = cpu; - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), value_boost, ) diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..f3eeb2c --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,55 @@ +use std::{fs, io, path::Path}; + +use anyhow::Context; + +pub fn exists(path: impl AsRef) -> bool { + let path = path.as_ref(); + + path.exists() +} + +pub fn read_dir(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + + fs::read_dir(path) + .with_context(|| format!("failed to read directory '{path}'", path = path.display())) +} + +pub fn read(path: impl AsRef) -> anyhow::Result> { + let path = path.as_ref(); + + match fs::read_to_string(path) { + Ok(string) => Ok(Some(string)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => { + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) + } + } +} + +pub fn read_u64(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + + let content = fs::read_to_string(path) + .with_context(|| format!("failed to read '{path}'", path = path.display()))?; + + Ok(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })?) +} + +pub 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(), + ) + }) +} diff --git a/src/main.rs b/src/main.rs index 0725e38..825465d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ +mod cpu; +mod power_supply; +mod system; + +mod fs; + mod config; // mod core; -mod cpu; mod daemon; // mod engine; // mod monitor; -mod power_supply; use anyhow::Context; use clap::Parser as _; diff --git a/src/power_supply.rs b/src/power_supply.rs index 1f69a3c..4e66bec 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,22 +1,12 @@ -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; use yansi::Paint as _; use std::{ - fmt, fs, + fmt, 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(), - ) - }) -} +use crate::fs; /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. @@ -136,10 +126,10 @@ impl PowerSupply { fn get_type(&self) -> anyhow::Result { let type_path = self.path.join("type"); - let type_ = fs::read_to_string(&type_path) + let type_ = fs::read(&type_path) .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - Ok(type_) + type_.ok_or_else(|| anyhow!("'{path}' doesn't exist", path = type_path.display())) } pub fn rescan(&mut self) -> anyhow::Result<()> { @@ -180,9 +170,9 @@ impl PowerSupply { } pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { - write( + fs::write( &self.charge_threshold_path_start().ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "power supply '{name}' does not support changing charge threshold levels", name = self.name, ) @@ -197,9 +187,9 @@ impl PowerSupply { } pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { - write( + fs::write( &self.charge_threshold_path_end().ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "power supply '{name}' does not support changing charge threshold levels", name = self.name, ) @@ -216,7 +206,7 @@ impl PowerSupply { pub fn get_available_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Ok(content) = fs::read_to_string(path) else { + let Ok(Some(content)) = fs::read(path) else { return Vec::new(); }; @@ -245,7 +235,7 @@ impl PowerSupply { ); } - write("/sys/firmware/acpi/platform_profile", profile) + fs::write("/sys/firmware/acpi/platform_profile", profile) .context("this probably means that your system does not support changing ACPI profiles") } } diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..1d3e697 --- /dev/null +++ b/src/system.rs @@ -0,0 +1,14 @@ +pub struct System { + pub is_desktop: bool, +} + +impl System { + pub fn new() -> anyhow::Result { + let mut system = Self { is_desktop: false }; + system.rescan()?; + + Ok(system) + } + + pub fn rescan(&mut self) -> anyhow::Result<()> {} +} From d5dbb36de4dc594bf46a4a377ad1d28ac39da864 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:12:58 +0300 Subject: [PATCH 21/93] fs: fix read() typesig --- src/cpu.rs | 4 ++-- src/fs.rs | 12 ++++++------ src/power_supply.rs | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 7b8ef99..0179746 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -86,7 +86,7 @@ impl Cpu { pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; - let Ok(Some(content)) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" )) else { return Vec::new(); @@ -127,7 +127,7 @@ impl Cpu { pub fn get_available_epps(&self) -> Vec { let Self { number, .. } = self; - let Ok(Some(content)) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" )) else { return Vec::new(); diff --git a/src/fs.rs b/src/fs.rs index f3eeb2c..b1d1c71 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -15,17 +15,17 @@ pub fn read_dir(path: impl AsRef) -> anyhow::Result { .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } -pub fn read(path: impl AsRef) -> anyhow::Result> { +pub fn read(path: impl AsRef) -> Option> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Ok(Some(string)), + Ok(string) => Some(Ok(string)), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Some( + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), + ), } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 4e66bec..f1dcb41 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -127,9 +127,10 @@ impl PowerSupply { let type_path = self.path.join("type"); let type_ = fs::read(&type_path) + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - type_.ok_or_else(|| anyhow!("'{path}' doesn't exist", path = type_path.display())) + Ok(type_) } pub fn rescan(&mut self) -> anyhow::Result<()> { @@ -206,7 +207,7 @@ impl PowerSupply { pub fn get_available_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Ok(Some(content)) = fs::read(path) else { + let Some(Ok(content)) = fs::read(path) else { return Vec::new(); }; From ec3452601267f39fc0c75c4d872d259f5a8d6245 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:41:53 +0300 Subject: [PATCH 22/93] cpu&power: add more attributes --- src/fs.rs | 19 ++++---- src/main.rs | 2 +- src/monitor.rs | 104 -------------------------------------------- src/power_supply.rs | 48 ++++++++++++++++++++ src/system.rs | 98 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 116 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index b1d1c71..9b150b3 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -29,18 +29,19 @@ pub fn read(path: impl AsRef) -> Option> { } } -pub fn read_u64(path: impl AsRef) -> anyhow::Result { +pub fn read_u64(path: impl AsRef) -> Option> { let path = path.as_ref(); - let content = fs::read_to_string(path) - .with_context(|| format!("failed to read '{path}'", path = path.display()))?; + match read(path)? { + Ok(content) => Some(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })), - Ok(content.trim().parse().with_context(|| { - format!( - "failed to parse contents of '{path}' as a unsigned number", - path = path.display(), - ) - })?) + Err(error) => Some(Err(error)), + } } pub fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { diff --git a/src/main.rs b/src/main.rs index 825465d..cd6258f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ enum Command { /// Start the daemon. Start { /// The daemon config path. - #[arg(long, env = "SUPERFREQ_CONFIG")] + #[arg(long, env = "WATT_CONFIG")] config: PathBuf, }, diff --git a/src/monitor.rs b/src/monitor.rs index 79d2635..cda52dc 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -599,110 +599,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> Ok(batteries) } -/// Check if a battery is likely a peripheral (mouse, keyboard, etc) not a laptop battery -fn is_peripheral_battery(ps_path: &Path, name: &str) -> bool { - // Convert name to lowercase once for case-insensitive matching - let name_lower = name.to_lowercase(); - - // Common peripheral battery names - if name_lower.contains("mouse") - || name_lower.contains("keyboard") - || name_lower.contains("trackpad") - || name_lower.contains("gamepad") - || name_lower.contains("controller") - || name_lower.contains("headset") - || name_lower.contains("headphone") - { - return true; - } - - // Small capacity batteries are likely not laptop batteries - if let Ok(energy_full) = read_sysfs_value::(ps_path.join("energy_full")) { - // Most laptop batteries are at least 20,000,000 µWh (20 Wh) - // Peripheral batteries are typically much smaller - if energy_full < 10_000_000 { - // 10 Wh in µWh - return true; - } - } - - // Check for model name that indicates a peripheral - if let Ok(model) = read_sysfs_file_trimmed(ps_path.join("model_name")) { - if model.contains("bluetooth") || model.contains("wireless") { - return true; - } - } - - false -} - -/// Determine if this is likely a desktop system rather than a laptop -fn is_likely_desktop_system() -> bool { - // Check for DMI system type information - if let Ok(chassis_type) = fs::read_to_string("/sys/class/dmi/id/chassis_type") { - let chassis_type = chassis_type.trim(); - - // Chassis types: - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One - // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis - match chassis_type { - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return true, // desktop form factors - "9" | "10" | "14" => return false, // laptop form factors - _ => {} // Unknown, continue with other checks - } - } - - // Check CPU power policies, desktops often don't have these - let power_saving_exists = Path::new("/sys/module/intel_pstate/parameters/no_hwp").exists() - || Path::new("/sys/devices/system/cpu/cpufreq/conservative").exists(); - - if !power_saving_exists { - return true; // likely a desktop - } - - // Check battery-specific ACPI paths that laptops typically have - let laptop_acpi_paths = [ - "/sys/class/power_supply/BAT0", - "/sys/class/power_supply/BAT1", - "/proc/acpi/battery", - ]; - - for path in &laptop_acpi_paths { - if Path::new(path).exists() { - return false; // Likely a laptop - } - } - - // Default to assuming desktop if we can't determine - true -} - -pub fn get_system_load() -> anyhow::Result { - let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; - let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); - if parts.len() < 3 { - return Err(SysMonitorError::ParseError( - "Could not parse /proc/loadavg: expected at least 3 parts".to_string(), - )); - } - let load_avg_1min = parts[0].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0])) - })?; - let load_avg_5min = parts[1].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1])) - })?; - let load_avg_15min = parts[2].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2])) - })?; - - Ok(SystemLoad { - load_avg_1min, - load_avg_5min, - load_avg_15min, - }) -} - pub fn collect_system_report(config: &AppConfig) -> anyhow::Result { let system_info = get_system_info(); let cpu_cores = get_all_cpu_core_info()?; diff --git a/src/power_supply.rs b/src/power_supply.rs index f1dcb41..025ac75 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -48,6 +48,9 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ pub struct PowerSupply { pub name: String, pub path: PathBuf, + + pub is_from_peripheral: bool, + pub threshold_config: Option, } @@ -74,6 +77,9 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, + + is_from_peripheral: false, + threshold_config: None, }; @@ -94,6 +100,8 @@ impl PowerSupply { path, + is_from_peripheral: false, + threshold_config: None, }; @@ -157,6 +165,46 @@ impl PowerSupply { self.threshold_config = threshold_config; + self.is_from_peripheral = 'is_from_peripheral: { + let name_lower = self.name.to_lowercase(); + + // Common peripheral battery names. + if name_lower.contains("mouse") + || name_lower.contains("keyboard") + || name_lower.contains("trackpad") + || name_lower.contains("gamepad") + || name_lower.contains("controller") + || name_lower.contains("headset") + || name_lower.contains("headphone") + { + break 'is_from_peripheral true; + } + + // Small capacity batteries are likely not laptop batteries. + if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { + let energy_full = energy_full + .with_context(|| format!("failed to read the max charge '{self}' can hold"))?; + + // Most laptop batteries are at least 20,000,000 µWh (20 Wh). + // Peripheral batteries are typically much smaller. + if energy_full < 10_000_000 { + // 10 Wh in µWh. + break 'is_from_peripheral true; + } + } + // Check for model name that indicates a peripheral + if let Some(model) = fs::read(self.path.join("model_name")) { + let model = + model.with_context(|| format!("failed to read the model name of '{self}'"))?; + + if model.contains("bluetooth") || model.contains("wireless") { + break 'is_from_peripheral true; + } + } + + false + }; + Ok(()) } diff --git a/src/system.rs b/src/system.rs index 1d3e697..cc39776 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,14 +1,108 @@ +use anyhow::{Context, bail}; + +use crate::fs; + pub struct System { pub is_desktop: bool, + + pub load_average_1min: f64, + pub load_average_5min: f64, + pub load_average_15min: f64, } impl System { pub fn new() -> anyhow::Result { - let mut system = Self { is_desktop: false }; + let mut system = Self { + is_desktop: false, + + load_average_1min: 0.0, + load_average_5min: 0.0, + load_average_15min: 0.0, + }; + system.rescan()?; Ok(system) } - pub fn rescan(&mut self) -> anyhow::Result<()> {} + pub fn rescan(&mut self) -> anyhow::Result<()> { + self.is_desktop = self.is_desktop()?; + + let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?; + self.load_average_1min = load_average_1min; + self.load_average_5min = load_average_5min; + self.load_average_15min = load_average_15min; + + Ok(()) + } + + fn is_desktop(&self) -> anyhow::Result { + if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { + let chassis_type = chassis_type.context("failed to read chassis type")?; + + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis + match chassis_type.trim() { + // Desktop form factors. + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true), + // Laptop form factors. + "9" | "10" | "14" => return Ok(false), + // Unknown, continue with other checks + _ => {} + } + } + + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + return Ok(true); // Likely a desktop. + } + + // Check battery-specific ACPI paths that laptops typically have + let laptop_acpi_paths = [ + "/sys/class/power_supply/BAT0", + "/sys/class/power_supply/BAT1", + "/proc/acpi/battery", + ]; + + for path in laptop_acpi_paths { + if fs::exists(path) { + return Ok(false); // Likely a laptop. + } + } + + // Default to assuming desktop if we can't determine. + Ok(true) + } + + fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> { + let content = fs::read("/proc/loadavg") + .context("load average file doesn't exist, are you on linux?")? + .context("failed to read load average")?; + + let mut parts = content.split_whitespace(); + + let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) = + (parts.next(), parts.next(), parts.next()) + else { + bail!( + "failed to parse first 3 load average entries due to there not being enough, content: {content}" + ); + }; + + Ok(( + load_average_1min + .parse() + .context("failed to parse load average")?, + load_average_5min + .parse() + .context("failed to parse load average")?, + load_average_15min + .parse() + .context("failed to parse load average")?, + )) + } } From 45a9fd4749edc9fe9c48bff27d5cd8cc7c4de1d5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:24:36 +0300 Subject: [PATCH 23/93] cpu: cpu times --- src/cpu.rs | 105 +++++++++++++++++++++++++++++++++++++++++--- src/monitor.rs | 105 -------------------------------------------- src/power_supply.rs | 4 +- src/system.rs | 54 +++++++++++++---------- 4 files changed, 131 insertions(+), 137 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 0179746..2e0db57 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,10 +5,37 @@ use std::{fmt, string::ToString}; use crate::fs; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Cpu { pub number: u32, + pub has_cpufreq: bool, + + pub time_user: u64, + pub time_nice: u64, + pub time_system: u64, + pub time_idle: u64, + pub time_iowait: u64, + pub time_irq: u64, + pub time_softirq: u64, + pub time_steal: u64, +} + +impl Cpu { + pub fn time_total(&self) -> u64 { + self.time_user + + self.time_nice + + self.time_system + + self.time_idle + + self.time_iowait + + self.time_irq + + self.time_softirq + + self.time_steal + } + + pub fn time_idle(&self) -> u64 { + self.time_idle + self.time_iowait + } } impl fmt::Display for Cpu { @@ -24,6 +51,15 @@ impl Cpu { let mut cpu = Self { number, has_cpufreq: false, + + time_user: 0, + time_nice: 0, + time_system: 0, + time_idle: 0, + time_iowait: 0, + time_irq: 0, + time_softirq: 0, + time_steal: 0, }; cpu.rescan()?; @@ -76,9 +112,68 @@ impl Cpu { bail!("{self} does not exist"); } - let has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); + self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.has_cpufreq = has_cpufreq; + self.rescan_times()?; + + Ok(()) + } + + fn rescan_times(&mut self) -> anyhow::Result<()> { + let content = fs::read("/proc/stat") + .context("/proc/stat does not exist")? + .context("failed to read CPU stat")?; + + let cpu_name = format!("cpu{number}", number = self.number); + + let mut stats = content + .lines() + .find_map(|line| { + line.starts_with(&cpu_name) + .then(|| line.split_whitespace().skip(1)) + }) + .with_context(|| format!("failed to find {self} in CPU stats"))?; + + self.time_user = stats + .next() + .with_context(|| format!("failed to find {self} user time"))? + .parse() + .with_context(|| format!("failed to parse {self} user time"))?; + self.time_nice = stats + .next() + .with_context(|| format!("failed to find {self} nice time"))? + .parse() + .with_context(|| format!("failed to parse {self} nice time"))?; + self.time_system = stats + .next() + .with_context(|| format!("failed to find {self} system time"))? + .parse() + .with_context(|| format!("failed to parse {self} system time"))?; + self.time_idle = stats + .next() + .with_context(|| format!("failed to find {self} idle time"))? + .parse() + .with_context(|| format!("failed to parse {self} idle time"))?; + self.time_iowait = stats + .next() + .with_context(|| format!("failed to find {self} iowait time"))? + .parse() + .with_context(|| format!("failed to parse {self} iowait time"))?; + self.time_irq = stats + .next() + .with_context(|| format!("failed to find {self} irq time"))? + .parse() + .with_context(|| format!("failed to parse {self} irq time"))?; + self.time_softirq = stats + .next() + .with_context(|| format!("failed to find {self} softirq time"))? + .parse() + .with_context(|| format!("failed to parse {self} softirq time"))?; + self.time_steal = stats + .next() + .with_context(|| format!("failed to find {self} steal time"))? + .parse() + .with_context(|| format!("failed to parse {self} steal time"))?; Ok(()) } @@ -232,7 +327,7 @@ impl Cpu { fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(minimum_frequency_khz) = fs::read_u64(format!( + let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -270,7 +365,7 @@ impl Cpu { fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(maximum_frequency_khz) = fs::read_u64(format!( + let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. diff --git a/src/monitor.rs b/src/monitor.rs index cda52dc..1cd0dc4 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -43,111 +43,6 @@ pub fn get_system_info() -> SystemInfo { } } -#[derive(Debug, Clone, Copy)] -pub struct CpuTimes { - user: u64, - nice: u64, - system: u64, - idle: u64, - iowait: u64, - irq: u64, - softirq: u64, - steal: u64, -} - -impl CpuTimes { - const fn total_time(&self) -> u64 { - self.user - + self.nice - + self.system - + self.idle - + self.iowait - + self.irq - + self.softirq - + self.steal - } - - const fn idle_time(&self) -> u64 { - self.idle + self.iowait - } -} - -fn read_all_cpu_times() -> anyhow::Result> { - let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?; - let mut cpu_times_map = HashMap::new(); - - for line in content.lines() { - if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() < 11 { - return Err(SysMonitorError::ProcStatParseError(format!( - "Line too short: {line}" - ))); - } - - let core_id_str = &parts[0][3..]; - let core_id = core_id_str.parse::().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse core_id: {core_id_str}" - )) - })?; - - let times = CpuTimes { - user: parts[1].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse user time: {}", - parts[1] - )) - })?, - nice: parts[2].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse nice time: {}", - parts[2] - )) - })?, - system: parts[3].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse system time: {}", - parts[3] - )) - })?, - idle: parts[4].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse idle time: {}", - parts[4] - )) - })?, - iowait: parts[5].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse iowait time: {}", - parts[5] - )) - })?, - irq: parts[6].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse irq time: {}", - parts[6] - )) - })?, - softirq: parts[7].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse softirq time: {}", - parts[7] - )) - })?, - steal: parts[8].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse steal time: {}", - parts[8] - )) - })?, - }; - cpu_times_map.insert(core_id, times); - } - } - Ok(cpu_times_map) -} - pub fn get_cpu_core_info( core_id: u32, prev_times: &CpuTimes, diff --git a/src/power_supply.rs b/src/power_supply.rs index 025ac75..5bbcebc 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -146,7 +146,7 @@ impl PowerSupply { bail!("{self} does not exist"); } - let threshold_config = self + self.threshold_config = self .get_type() .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? .eq("Battery") @@ -163,8 +163,6 @@ impl PowerSupply { }) .flatten(); - self.threshold_config = threshold_config; - self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); diff --git a/src/system.rs b/src/system.rs index cc39776..f8820b1 100644 --- a/src/system.rs +++ b/src/system.rs @@ -26,17 +26,13 @@ impl System { } pub fn rescan(&mut self) -> anyhow::Result<()> { - self.is_desktop = self.is_desktop()?; - - let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?; - self.load_average_1min = load_average_1min; - self.load_average_5min = load_average_5min; - self.load_average_15min = load_average_15min; + self.rescan_is_desktop()?; + self.rescan_load_average()?; Ok(()) } - fn is_desktop(&self) -> anyhow::Result { + fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { let chassis_type = chassis_type.context("failed to read chassis type")?; @@ -45,9 +41,16 @@ impl System { // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis match chassis_type.trim() { // Desktop form factors. - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true), + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { + self.is_desktop = true; + return Ok(()); + } // Laptop form factors. - "9" | "10" | "14" => return Ok(false), + "9" | "10" | "14" => { + self.is_desktop = false; + return Ok(()); + } + // Unknown, continue with other checks _ => {} } @@ -58,7 +61,8 @@ impl System { || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); if !power_saving_exists { - return Ok(true); // Likely a desktop. + self.is_desktop = true; + return Ok(()); // Likely a desktop. } // Check battery-specific ACPI paths that laptops typically have @@ -70,15 +74,17 @@ impl System { for path in laptop_acpi_paths { if fs::exists(path) { - return Ok(false); // Likely a laptop. + self.is_desktop = false; // Likely a laptop. + return Ok(()); } } // Default to assuming desktop if we can't determine. - Ok(true) + self.is_desktop = true; + Ok(()) } - fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> { + fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") .context("load average file doesn't exist, are you on linux?")? .context("failed to read load average")?; @@ -93,16 +99,16 @@ impl System { ); }; - Ok(( - load_average_1min - .parse() - .context("failed to parse load average")?, - load_average_5min - .parse() - .context("failed to parse load average")?, - load_average_15min - .parse() - .context("failed to parse load average")?, - )) + self.load_average_1min = load_average_1min + .parse() + .context("failed to parse load average")?; + self.load_average_5min = load_average_5min + .parse() + .context("failed to parse load average")?; + self.load_average_15min = load_average_15min + .parse() + .context("failed to parse load average")?; + + Ok(()) } } From 542c41ccbe2335ba686a9ae9713bb64aa29a308d Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:28:06 +0300 Subject: [PATCH 24/93] cpu: add TODO --- src/cpu.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cpu.rs b/src/cpu.rs index 2e0db57..736008a 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -120,6 +120,8 @@ impl Cpu { } fn rescan_times(&mut self) -> anyhow::Result<()> { + // TODO: Don't read this per CPU. Share the read or + // find something in /sys/.../cpu{N} that does it. let content = fs::read("/proc/stat") .context("/proc/stat does not exist")? .context("failed to read CPU stat")?; From fb5a891d423e941789e689b7003ccdd2fac1229f Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:37:10 +0300 Subject: [PATCH 25/93] main: use yansi::whenever --- Cargo.lock | 22 +++++++++++++++++++++- Cargo.toml | 2 +- src/main.rs | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b0446d..f077741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + [[package]] name = "indexmap" version = "2.9.0" @@ -271,6 +277,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.1", + "libc", + "windows-sys", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -364,7 +381,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -697,3 +714,6 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] diff --git a/Cargo.toml b/Cargo.toml index 287929e..aeecd4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,5 @@ thiserror = "2.0" anyhow = "1.0" jiff = "0.2.13" clap-verbosity-flag = "3.0.2" -yansi = "1.0.1" +yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } derive_more = { version = "2.0.1", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index cd6258f..e435cee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,8 @@ enum Command { fn real_main() -> anyhow::Result<()> { let cli = Cli::parse(); + yansi::whenever(yansi::Condition::TTY_AND_COLOR); + env_logger::Builder::new() .filter_level(cli.verbosity.log_level_filter()) .format_timestamp(None) From dfa788009c498a123c2e134c809983be8ecddac1 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 01:01:45 +0300 Subject: [PATCH 26/93] monitor: delete old code --- src/core.rs | 2 -- src/monitor.rs | 71 +------------------------------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) diff --git a/src/core.rs b/src/core.rs index 07581aa..a3f4e33 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,8 +1,6 @@ pub struct SystemInfo { // Overall system details pub cpu_model: String, - pub architecture: String, - pub linux_distribution: String, } pub struct CpuCoreInfo { diff --git a/src/monitor.rs b/src/monitor.rs index 1cd0dc4..5d0468b 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -10,37 +10,10 @@ use std::{ time::SystemTime, }; -// Read a sysfs file to a string, trimming whitespace -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| { - SysMonitorError::ReadError(format!("Path: {:?}, Error: {}", path.as_ref().display(), e)) - }) -} - -// Read a sysfs file and parse it to a specific type -fn read_sysfs_value(path: impl AsRef) -> anyhow::Result { - let content = read_sysfs_file_trimmed(path.as_ref())?; - content.parse::().map_err(|_| { - SysMonitorError::ParseError(format!( - "Could not parse '{}' from {:?}", - content, - path.as_ref().display() - )) - }) -} - pub fn get_system_info() -> SystemInfo { let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string()); - let linux_distribution = get_linux_distribution().unwrap_or_else(|_| "Unknown".to_string()); - let architecture = std::env::consts::ARCH.to_string(); - SystemInfo { - cpu_model, - architecture, - linux_distribution, - } + SystemInfo { cpu_model } } pub fn get_cpu_core_info( @@ -529,45 +502,3 @@ pub fn get_cpu_model() -> anyhow::Result { "Could not find CPU model name in /proc/cpuinfo.".to_string(), )) } - -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!( - "Cannot read contents of {}.", - os_release_path.display() - )) - })?; - - for line in content.lines() { - if line.starts_with("PRETTY_NAME=") { - if let Some(val) = line.split('=').nth(1) { - let linux_distribution = val.trim_matches('"').to_string(); - return Ok(linux_distribution); - } - } - } - - let lsb_release_path = Path::new("/etc/lsb-release"); - let content = fs::read_to_string(lsb_release_path).map_err(|_| { - SysMonitorError::ReadError(format!( - "Cannot read contents of {}.", - lsb_release_path.display() - )) - })?; - - for line in content.lines() { - if line.starts_with("DISTRIB_DESCRIPTION=") { - if let Some(val) = line.split('=').nth(1) { - let linux_distribution = val.trim_matches('"').to_string(); - return Ok(linux_distribution); - } - } - } - - Err(SysMonitorError::ParseError(format!( - "Could not find distribution name in {} or {}.", - os_release_path.display(), - lsb_release_path.display() - ))) -} From 55e04ea09e04808ca227f09e29fb904ab3dbfd47 Mon Sep 17 00:00:00 2001 From: Bloxx12 Date: Wed, 21 May 2025 14:35:59 +0200 Subject: [PATCH 27/93] flake: add aarch64-linux to systems --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index b5af16a..affc70b 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,7 @@ nixpkgs, ... } @ inputs: let - forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux"]; + forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"]; pkgsForEach = nixpkgs.legacyPackages; in { packages = forAllSystems (system: { From c1a509328be30951fdf009370672a9a25cad8634 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 17:47:13 +0300 Subject: [PATCH 28/93] cpu: store frequency --- src/config.rs | 8 +++---- src/core.rs | 3 --- src/cpu.rs | 63 ++++++++++++++++++++++++++++++++++++++++++-------- src/monitor.rs | 15 ------------ 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9ca0498..b68485a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,7 @@ pub struct CpuDelta { impl CpuDelta { pub fn apply(&self) -> anyhow::Result<()> { - let cpus = match &self.for_ { + let mut cpus = match &self.for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); @@ -63,7 +63,7 @@ impl CpuDelta { None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, }; - for cpu in cpus { + for cpu in &mut cpus { if let Some(governor) = self.governor.as_ref() { cpu.set_governor(governor)?; } @@ -77,11 +77,11 @@ impl CpuDelta { } if let Some(mhz_minimum) = self.frequency_mhz_minimum { - cpu.set_frequency_minimum(mhz_minimum)?; + cpu.set_frequency_mhz_minimum(mhz_minimum)?; } if let Some(mhz_maximum) = self.frequency_mhz_maximum { - cpu.set_frequency_maximum(mhz_maximum)?; + cpu.set_frequency_mhz_maximum(mhz_maximum)?; } } diff --git a/src/core.rs b/src/core.rs index a3f4e33..84f1886 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,9 +6,6 @@ pub struct SystemInfo { pub struct CpuCoreInfo { // Per-core data pub core_id: u32, - pub current_frequency_mhz: Option, - pub min_frequency_mhz: Option, - pub max_frequency_mhz: Option, pub usage_percent: Option, pub temperature_celsius: Option, } diff --git a/src/cpu.rs b/src/cpu.rs index 736008a..9941020 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -11,6 +11,10 @@ pub struct Cpu { pub has_cpufreq: bool, + pub frequency_mhz: u64, + pub frequency_mhz_minimum: u64, + pub frequency_mhz_maximum: u64, + pub time_user: u64, pub time_nice: u64, pub time_system: u64, @@ -52,6 +56,10 @@ impl Cpu { number, has_cpufreq: false, + frequency_mhz: 0, + frequency_mhz_minimum: 0, + frequency_mhz_maximum: 0, + time_user: 0, time_nice: 0, time_system: 0, @@ -115,6 +123,7 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.rescan_times()?; + self.rescan_frequency()?; Ok(()) } @@ -180,6 +189,32 @@ impl Cpu { Ok(()) } + fn rescan_frequency(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + let frequency_khz = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" + )) + .with_context(|| format!("failed to find {self} frequency"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_minimum = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) + .with_context(|| format!("failed to find {self} frequency minimum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_maximum = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to find {self} frequency maximum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + + self.frequency_mhz = frequency_khz / 1000; + self.frequency_mhz_minimum = frequency_khz_minimum / 1000; + self.frequency_mhz_maximum = frequency_khz_maximum / 1000; + + Ok(()) + } + pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; @@ -308,10 +343,10 @@ impl Cpu { }) } - pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = self; + pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.validate_frequency_minimum(frequency_mhz)?; + self.validate_frequency_mhz_minimum(frequency_mhz)?; // We use u64 for the intermediate calculation to prevent overflow let frequency_khz = frequency_mhz * 1000; @@ -323,10 +358,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") - }) + })?; + + self.frequency_mhz_minimum = frequency_mhz; + + Ok(()) } - fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( @@ -346,10 +385,10 @@ impl Cpu { Ok(()) } - pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = self; + pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.validate_frequency_maximum(frequency_mhz)?; + self.validate_frequency_mhz_maximum(frequency_mhz)?; // We use u64 for the intermediate calculation to prevent overflow let frequency_khz = frequency_mhz * 1000; @@ -361,10 +400,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") - }) + })?; + + self.frequency_mhz_maximum = frequency_mhz; + + Ok(()) } - fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( diff --git a/src/monitor.rs b/src/monitor.rs index 5d0468b..9f0c972 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -21,18 +21,6 @@ pub fn get_cpu_core_info( prev_times: &CpuTimes, current_times: &CpuTimes, ) -> 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")) - .map(|khz| khz / 1000) - .ok(); - let min_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_min_freq")) - .map(|khz| khz / 1000) - .ok(); - let max_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_max_freq")) - .map(|khz| khz / 1000) - .ok(); - // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -144,9 +132,6 @@ pub fn get_cpu_core_info( Ok(CpuCoreInfo { core_id, - current_frequency_mhz, - min_frequency_mhz, - max_frequency_mhz, usage_percent, temperature_celsius, }) From 3212bc0ad56d6425c9c81b8c1374335a2319d4c6 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 18:27:09 +0300 Subject: [PATCH 29/93] cpu: store governor and available governors --- src/core.rs | 2 -- src/cpu.rs | 92 ++++++++++++++++++++++++++++++++------------------ src/fs.rs | 2 +- src/monitor.rs | 21 ------------ 4 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/core.rs b/src/core.rs index 84f1886..38c3d0c 100644 --- a/src/core.rs +++ b/src/core.rs @@ -12,8 +12,6 @@ pub struct CpuCoreInfo { pub struct CpuGlobalInfo { // System-wide CPU settings - pub current_governor: Option, - pub available_governors: Vec, pub turbo_status: Option, // true for enabled, false for disabled pub epp: Option, // Energy Performance Preference pub epb: Option, // Energy Performance Bias diff --git a/src/cpu.rs b/src/cpu.rs index 9941020..7365940 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -11,9 +11,12 @@ pub struct Cpu { pub has_cpufreq: bool, - pub frequency_mhz: u64, - pub frequency_mhz_minimum: u64, - pub frequency_mhz_maximum: u64, + pub available_governors: Vec, + pub governor: Option, + + pub frequency_mhz: Option, + pub frequency_mhz_minimum: Option, + pub frequency_mhz_maximum: Option, pub time_user: u64, pub time_nice: u64, @@ -56,9 +59,12 @@ impl Cpu { number, has_cpufreq: false, - frequency_mhz: 0, - frequency_mhz_minimum: 0, - frequency_mhz_maximum: 0, + available_governors: Vec::new(), + governor: None, + + frequency_mhz: None, + frequency_mhz_minimum: None, + frequency_mhz_maximum: None, time_user: 0, time_nice: 0, @@ -123,7 +129,11 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.rescan_times()?; - self.rescan_frequency()?; + + if self.has_cpufreq { + self.rescan_governor()?; + self.rescan_frequency()?; + } Ok(()) } @@ -189,6 +199,33 @@ impl Cpu { Ok(()) } + fn rescan_governor(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + self.available_governors = 'available_governors: { + let Some(Ok(content)) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" + )) else { + break 'available_governors Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; + + self.governor = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" + )) + .with_context(|| format!("failed to find {self} scaling governor"))? + .with_context(|| format!("failed to read {self} scaling governor"))?, + ); + + Ok(()) + } + fn rescan_frequency(&mut self) -> anyhow::Result<()> { let Self { number, .. } = *self; @@ -208,32 +245,19 @@ impl Cpu { .with_context(|| format!("failed to find {self} frequency maximum"))? .with_context(|| format!("failed to parse {self} frequency"))?; - self.frequency_mhz = frequency_khz / 1000; - self.frequency_mhz_minimum = frequency_khz_minimum / 1000; - self.frequency_mhz_maximum = frequency_khz_maximum / 1000; + self.frequency_mhz = Some(frequency_khz / 1000); + self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); + self.frequency_mhz_maximum = Some(frequency_khz_maximum / 1000); Ok(()) } - pub fn get_available_governors(&self) -> Vec { - let Self { number, .. } = self; - - let Some(Ok(content)) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" - )) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - } - - pub fn set_governor(&self, governor: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let governors = self.get_available_governors(); + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { + let Self { + number, + available_governors: ref governors, + .. + } = *self; if !governors .iter() @@ -253,7 +277,11 @@ impl Cpu { format!( "this probably means that {self} doesn't exist or doesn't support changing governors" ) - }) + })?; + + self.governor = Some(governor.to_owned()); + + Ok(()) } pub fn get_available_epps(&self) -> Vec { @@ -360,7 +388,7 @@ impl Cpu { format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") })?; - self.frequency_mhz_minimum = frequency_mhz; + self.frequency_mhz_minimum = Some(frequency_mhz); Ok(()) } @@ -402,7 +430,7 @@ impl Cpu { format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") })?; - self.frequency_mhz_maximum = frequency_mhz; + self.frequency_mhz_maximum = Some(frequency_mhz); Ok(()) } diff --git a/src/fs.rs b/src/fs.rs index 9b150b3..4c11178 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -19,7 +19,7 @@ pub fn read(path: impl AsRef) -> Option> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Some(Ok(string)), + Ok(string) => Some(Ok(string.trim().to_owned())), Err(error) if error.kind() == io::ErrorKind::NotFound => None, diff --git a/src/monitor.rs b/src/monitor.rs index 9f0c972..44a7146 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -260,25 +260,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); - let current_governor = if cpufreq_base_path_buf.join("scaling_governor").exists() { - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("scaling_governor")).ok() - } else { - None - }; - - let available_governors = if cpufreq_base_path_buf - .join("scaling_available_governors") - .exists() - { - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("scaling_available_governors")) - .map_or_else( - |_| vec![], - |s| s.split_whitespace().map(String::from).collect(), - ) - } else { - vec![] - }; - let turbo_status = if turbo_status_path.exists() { // 0 means turbo enabled, 1 means disabled for intel_pstate read_sysfs_value::(turbo_status_path) @@ -325,8 +306,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - current_governor, - available_governors, turbo_status, epp: energy_perf_pref, epb: energy_perf_bias, From 24fa53914d16da87c43a1828d754ac20523d19fc Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 18:43:31 +0300 Subject: [PATCH 30/93] cpu: store EPP and EPB --- src/cpu.rs | 148 +++++++++++++++++++++++++++++++------------------ src/monitor.rs | 29 ---------- 2 files changed, 95 insertions(+), 82 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 7365940..c41a0d0 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -18,6 +18,12 @@ pub struct Cpu { pub frequency_mhz_minimum: Option, pub frequency_mhz_maximum: Option, + pub available_epps: Vec, + pub epp: Option, + + pub available_epbs: Vec, + pub epb: Option, + pub time_user: u64, pub time_nice: u64, pub time_system: u64, @@ -66,6 +72,12 @@ impl Cpu { frequency_mhz_minimum: None, frequency_mhz_maximum: None, + available_epps: Vec::new(), + epp: None, + + available_epbs: Vec::new(), + epb: None, + time_user: 0, time_nice: 0, time_system: 0, @@ -133,6 +145,8 @@ impl Cpu { if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; + self.rescan_epp()?; + self.rescan_epb()?; } Ok(()) @@ -252,6 +266,75 @@ impl Cpu { Ok(()) } + fn rescan_epp(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.available_epps = 'available_epps: { + let Some(Ok(content)) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" + )) else { + break 'available_epps Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; + + self.epp = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" + )) + .with_context(|| format!("failed to find {self} EPP"))? + .with_context(|| format!("failed to read {self} EPP"))?, + ); + + Ok(()) + } + + fn rescan_epb(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.available_epbs = if self.has_cpufreq { + vec![ + "1".to_owned(), + "2".to_owned(), + "3".to_owned(), + "4".to_owned(), + "5".to_owned(), + "6".to_owned(), + "7".to_owned(), + "8".to_owned(), + "9".to_owned(), + "10".to_owned(), + "11".to_owned(), + "12".to_owned(), + "13".to_owned(), + "14".to_owned(), + "15".to_owned(), + "performance".to_owned(), + "balance-performance".to_owned(), + "balance_performance".to_owned(), // Alternative form with underscore. + "balance-power".to_owned(), + "balance_power".to_owned(), // Alternative form with underscore. + "power".to_owned(), + ] + } else { + Vec::new() + }; + + self.epb = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" + )) + .with_context(|| format!("failed to find {self} EPB"))? + .with_context(|| format!("failed to read {self} EPB"))?, + ); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, @@ -284,25 +367,12 @@ impl Cpu { Ok(()) } - pub fn get_available_epps(&self) -> Vec { - let Self { number, .. } = self; - - let Some(Ok(content)) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" - )) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - } - - pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let epps = self.get_available_epps(); + pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { + let Self { + number, + available_epps: ref epps, + .. + } = *self; if !epps.iter().any(|avail_epp| avail_epp == epp) { bail!( @@ -320,42 +390,14 @@ impl Cpu { }) } - pub fn get_available_epbs(&self) -> &'static [&'static str] { - if !self.has_cpufreq { - return &[]; - } - - &[ - "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", - ] - } - pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; + let Self { + number, + available_epbs: ref epbs, + .. + } = *self; - let epbs = self.get_available_epbs(); - - if !epbs.contains(&epb) { + if !epbs.iter().any(|avail_epb| avail_epb == epb) { bail!( "EPB value '{epb}' is not available for {self}. available EPB values: {valid}", valid = epbs.join(", "), diff --git a/src/monitor.rs b/src/monitor.rs index 44a7146..609213d 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -238,25 +238,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - // Find a valid CPU to read global settings from - // Try cpu0 first, then fall back to any available CPU with cpufreq - let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/"); - - if !cpufreq_base_path_buf.exists() { - let core_count = get_real_cpus().unwrap_or_else(|e| { - eprintln!("Warning: {e}"); - 0 - }); - - for i in 0..core_count { - let test_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{i}/cpufreq/")); - if test_path.exists() { - cpufreq_base_path_buf = test_path; - break; // Exit the loop as soon as we find a valid path - } - } - } - let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); @@ -272,14 +253,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { None }; - // EPP (Energy Performance Preference) - let energy_perf_pref = - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("energy_performance_preference")).ok(); - - // EPB (Energy Performance Bias) - let energy_perf_bias = - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("energy_performance_bias")).ok(); - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); // Calculate average CPU temperature from the core temperatures @@ -307,8 +280,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { turbo_status, - epp: energy_perf_pref, - epb: energy_perf_bias, platform_profile, average_temperature_celsius, } From 6caa4f7941615c7c32ba9d49987a37bb7763a989 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 17:42:33 +0300 Subject: [PATCH 31/93] cpu: set_ep{p,b} actually sets the attributes now --- src/cpu.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index c41a0d0..d7ab01f 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -387,10 +387,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPP") - }) + })?; + + self.epp = Some(epp.to_owned()); + + Ok(()) } - pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { + pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { let Self { number, available_epbs: ref epbs, @@ -410,7 +414,11 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPB") - }) + })?; + + self.epb = Some(epb.to_owned()); + + Ok(()) } pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { From 3aec909d5db5db06c9060afb24d7a25394b624d2 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 17:47:21 +0300 Subject: [PATCH 32/93] cpu: add global turbo querying --- src/core.rs | 5 ++--- src/cpu.rs | 12 ++++++++++++ src/monitor.rs | 16 ---------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/core.rs b/src/core.rs index 38c3d0c..e3773c5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -12,9 +12,8 @@ pub struct CpuCoreInfo { pub struct CpuGlobalInfo { // System-wide CPU settings - pub turbo_status: Option, // true for enabled, false for disabled - pub epp: Option, // Energy Performance Preference - pub epb: Option, // Energy Performance Bias + pub epp: Option, // Energy Performance Preference + pub epb: Option, // Energy Performance Bias pub platform_profile: Option, pub average_temperature_celsius: Option, // Average temperature across all cores } diff --git a/src/cpu.rs b/src/cpu.rs index d7ab01f..3d6fd85 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -553,4 +553,16 @@ impl Cpu { bail!("no supported CPU boost control mechanism found"); } + + pub fn turbo() -> Option { + if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") { + return Some(content == 0); + } + + if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/cpufreq/boost") { + return Some(content == 1); + } + + None + } } diff --git a/src/monitor.rs b/src/monitor.rs index 609213d..4286f0b 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -238,21 +238,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); - let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); - - let turbo_status = if turbo_status_path.exists() { - // 0 means turbo enabled, 1 means disabled for intel_pstate - read_sysfs_value::(turbo_status_path) - .map(|val| val == 0) - .ok() - } else if boost_path.exists() { - // 1 means turbo enabled, 0 means disabled for generic cpufreq boost - read_sysfs_value::(boost_path).map(|val| val == 1).ok() - } else { - None - }; - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); // Calculate average CPU temperature from the core temperatures @@ -279,7 +264,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - turbo_status, platform_profile, average_temperature_celsius, } From 1fe3ca3bfdf7acdb33e1ca60cb5e64716e96235d Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 18:01:10 +0300 Subject: [PATCH 33/93] cpu: add usage percent --- src/core.rs | 1 - src/cpu.rs | 4 ++++ src/monitor.rs | 20 -------------------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/core.rs b/src/core.rs index e3773c5..bdd7a24 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,7 +6,6 @@ pub struct SystemInfo { pub struct CpuCoreInfo { // Per-core data pub core_id: u32, - pub usage_percent: Option, pub temperature_celsius: Option, } diff --git a/src/cpu.rs b/src/cpu.rs index 3d6fd85..89e9ea9 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -49,6 +49,10 @@ impl Cpu { pub fn time_idle(&self) -> u64 { self.time_idle + self.time_iowait } + + pub fn usage(&self) -> f64 { + 1.0 - self.time_idle() as f64 / self.time_total() as f64 + } } impl fmt::Display for Cpu { diff --git a/src/monitor.rs b/src/monitor.rs index 4286f0b..7ab1bb0 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -111,28 +111,8 @@ pub fn get_cpu_core_info( } } - let usage_percent: Option = { - let prev_idle = prev_times.idle_time(); - let current_idle = current_times.idle_time(); - - let prev_total = prev_times.total_time(); - let current_total = current_times.total_time(); - - let total_diff = current_total.saturating_sub(prev_total); - let idle_diff = current_idle.saturating_sub(prev_idle); - - // Avoid division by zero if no time has passed or counters haven't changed - if total_diff == 0 { - None - } else { - let usage = 100.0 * (1.0 - (idle_diff as f32 / total_diff as f32)); - Some(usage.clamp(0.0, 100.0)) // clamp between 0 and 100 - } - }; - Ok(CpuCoreInfo { core_id, - usage_percent, temperature_celsius, }) } From 29944b7dbb8012960fc5039a08fd16b7e24ec188 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 19:49:45 +0300 Subject: [PATCH 34/93] power_supply: add more stuff and store them --- src/config.rs | 8 +-- src/core.rs | 1 - src/cpu.rs | 108 +++++++++++++++------------- src/fs.rs | 25 ++++--- src/monitor.rs | 123 -------------------------------- src/power_supply.rs | 169 ++++++++++++++++++++++++++++++++------------ src/system.rs | 10 +-- 7 files changed, 207 insertions(+), 237 deletions(-) diff --git a/src/config.rs b/src/config.rs index b68485a..4de0ac3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,7 @@ pub struct PowerDelta { impl PowerDelta { pub fn apply(&self) -> anyhow::Result<()> { - let power_supplies = match &self.for_ { + let mut power_supplies = match &self.for_ { Some(names) => { let mut power_supplies = Vec::with_capacity(names.len()); @@ -136,13 +136,13 @@ impl PowerDelta { .collect(), }; - for power_supply in power_supplies { + for power_supply in &mut power_supplies { if let Some(threshold_start) = self.charge_threshold_start { - power_supply.set_charge_threshold_start(threshold_start)?; + power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?; } if let Some(threshold_end) = self.charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end)?; + power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?; } } diff --git a/src/core.rs b/src/core.rs index bdd7a24..2e32854 100644 --- a/src/core.rs +++ b/src/core.rs @@ -13,7 +13,6 @@ pub struct CpuGlobalInfo { // System-wide CPU settings pub epp: Option, // Energy Performance Preference pub epb: Option, // Energy Performance Bias - pub platform_profile: Option, pub average_temperature_celsius: Option, // Average temperature across all cores } diff --git a/src/cpu.rs b/src/cpu.rs index 89e9ea9..6712cdf 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -160,8 +160,8 @@ impl Cpu { // TODO: Don't read this per CPU. Share the read or // find something in /sys/.../cpu{N} that does it. let content = fs::read("/proc/stat") - .context("/proc/stat does not exist")? - .context("failed to read CPU stat")?; + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; let cpu_name = format!("cpu{number}", number = self.number); @@ -175,44 +175,44 @@ impl Cpu { self.time_user = stats .next() - .with_context(|| format!("failed to find {self} user time"))? + .with_context(|| format!("failed to parse {self} user time"))? .parse() - .with_context(|| format!("failed to parse {self} user time"))?; + .with_context(|| format!("failed to find {self} user time"))?; self.time_nice = stats .next() - .with_context(|| format!("failed to find {self} nice time"))? + .with_context(|| format!("failed to parse {self} nice time"))? .parse() - .with_context(|| format!("failed to parse {self} nice time"))?; + .with_context(|| format!("failed to find {self} nice time"))?; self.time_system = stats .next() - .with_context(|| format!("failed to find {self} system time"))? + .with_context(|| format!("failed to parse {self} system time"))? .parse() - .with_context(|| format!("failed to parse {self} system time"))?; + .with_context(|| format!("failed to find {self} system time"))?; self.time_idle = stats .next() - .with_context(|| format!("failed to find {self} idle time"))? + .with_context(|| format!("failed to parse {self} idle time"))? .parse() - .with_context(|| format!("failed to parse {self} idle time"))?; + .with_context(|| format!("failed to find {self} idle time"))?; self.time_iowait = stats .next() - .with_context(|| format!("failed to find {self} iowait time"))? + .with_context(|| format!("failed to parse {self} iowait time"))? .parse() - .with_context(|| format!("failed to parse {self} iowait time"))?; + .with_context(|| format!("failed to find {self} iowait time"))?; self.time_irq = stats .next() - .with_context(|| format!("failed to find {self} irq time"))? + .with_context(|| format!("failed to parse {self} irq time"))? .parse() - .with_context(|| format!("failed to parse {self} irq time"))?; + .with_context(|| format!("failed to find {self} irq time"))?; self.time_softirq = stats .next() - .with_context(|| format!("failed to find {self} softirq time"))? + .with_context(|| format!("failed to parse {self} softirq time"))? .parse() - .with_context(|| format!("failed to parse {self} softirq time"))?; + .with_context(|| format!("failed to find {self} softirq time"))?; self.time_steal = stats .next() - .with_context(|| format!("failed to find {self} steal time"))? + .with_context(|| format!("failed to parse {self} steal time"))? .parse() - .with_context(|| format!("failed to parse {self} steal time"))?; + .with_context(|| format!("failed to find {self} steal time"))?; Ok(()) } @@ -221,9 +221,11 @@ impl Cpu { let Self { number, .. } = *self; self.available_governors = 'available_governors: { - let Some(Ok(content)) = fs::read(format!( + let Some(content) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" - )) else { + )) + .with_context(|| format!("failed to read {self} available governors"))? + else { break 'available_governors Vec::new(); }; @@ -237,8 +239,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" )) - .with_context(|| format!("failed to find {self} scaling governor"))? - .with_context(|| format!("failed to read {self} scaling governor"))?, + .with_context(|| format!("failed to read {self} scaling governor"))? + .with_context(|| format!("failed to find {self} scaling governor"))?, ); Ok(()) @@ -247,21 +249,21 @@ impl Cpu { fn rescan_frequency(&mut self) -> anyhow::Result<()> { let Self { number, .. } = *self; - let frequency_khz = fs::read_u64(format!( + let frequency_khz = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" )) - .with_context(|| format!("failed to find {self} frequency"))? - .with_context(|| format!("failed to parse {self} frequency"))?; - let frequency_khz_minimum = fs::read_u64(format!( + .with_context(|| format!("failed to parse {self} frequency"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_minimum = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) - .with_context(|| format!("failed to find {self} frequency minimum"))? - .with_context(|| format!("failed to parse {self} frequency"))?; - let frequency_khz_maximum = fs::read_u64(format!( + .with_context(|| format!("failed to parse {self} frequency minimum"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_maximum = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" )) - .with_context(|| format!("failed to find {self} frequency maximum"))? - .with_context(|| format!("failed to parse {self} frequency"))?; + .with_context(|| format!("failed to parse {self} frequency maximum"))? + .with_context(|| format!("failed to find {self} frequency"))?; self.frequency_mhz = Some(frequency_khz / 1000); self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); @@ -271,12 +273,12 @@ impl Cpu { } fn rescan_epp(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = self; + let Self { number, .. } = *self; self.available_epps = 'available_epps: { - let Some(Ok(content)) = fs::read(format!( + let Some(content) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" - )) else { + )).with_context(|| format!("failed to read {self} available EPPs"))? else { break 'available_epps Vec::new(); }; @@ -290,8 +292,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" )) - .with_context(|| format!("failed to find {self} EPP"))? - .with_context(|| format!("failed to read {self} EPP"))?, + .with_context(|| format!("failed to read {self} EPP"))? + .with_context(|| format!("failed to find {self} EPP"))?, ); Ok(()) @@ -332,8 +334,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" )) - .with_context(|| format!("failed to find {self} EPB"))? - .with_context(|| format!("failed to read {self} EPB"))?, + .with_context(|| format!("failed to read {self} EPB"))? + .with_context(|| format!("failed to find {self} EPB"))?, ); Ok(()) @@ -450,9 +452,11 @@ impl Cpu { fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( + let Some(minimum_frequency_khz) = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) else { + )) + .with_context(|| format!("failed to read {self} minimum frequency"))? + else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -492,9 +496,11 @@ impl Cpu { fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) else { + let Some(maximum_frequency_khz) = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to read {self} maximum frequency"))? + else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -558,15 +564,19 @@ impl Cpu { bail!("no supported CPU boost control mechanism found"); } - pub fn turbo() -> Option { - if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") { - return Some(content == 0); + pub fn turbo() -> anyhow::Result> { + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/intel_pstate/no_turbo") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 0)); } - if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/cpufreq/boost") { - return Some(content == 1); + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/cpufreq/boost") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 1)); } - None + Ok(None) } } diff --git a/src/fs.rs b/src/fs.rs index 4c11178..526856d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,4 +1,4 @@ -use std::{fs, io, path::Path}; +use std::{error, fs, io, path::Path, str}; use anyhow::Context; @@ -15,32 +15,35 @@ pub fn read_dir(path: impl AsRef) -> anyhow::Result { .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } -pub fn read(path: impl AsRef) -> Option> { +pub fn read(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Some(Ok(string.trim().to_owned())), + Ok(string) => Ok(Some(string.trim().to_owned())), - Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => Some( - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), - ), + Err(error) => { + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) + } } } -pub fn read_u64(path: impl AsRef) -> Option> { +pub fn read_n(path: impl AsRef) -> anyhow::Result> +where + N::Err: error::Error + Send + Sync + 'static, +{ let path = path.as_ref(); match read(path)? { - Ok(content) => Some(content.trim().parse().with_context(|| { + Some(content) => Ok(Some(content.trim().parse().with_context(|| { format!( "failed to parse contents of '{path}' as a unsigned number", path = path.display(), ) - })), + })?)), - Err(error) => Some(Err(error)), + None => Ok(None), } } diff --git a/src/monitor.rs b/src/monitor.rs index 7ab1bb0..19cc69e 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -218,8 +218,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); - // Calculate average CPU temperature from the core temperatures let average_temperature_celsius = if cpu_cores.is_empty() { None @@ -244,120 +242,16 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - platform_profile, average_temperature_celsius, } } pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { - let mut batteries = Vec::new(); - let power_supply_path = Path::new("/sys/class/power_supply"); - - if !power_supply_path.exists() { - return Ok(batteries); // no power supply directory - } - - let ignored_supplies = config.ignored_power_supplies.clone().unwrap_or_default(); - - // Determine overall AC connection status - let mut overall_ac_connected = false; - for entry in fs::read_dir(power_supply_path)? { - let entry = entry?; - let ps_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - // Check for AC adapter type (common names: AC, ACAD, ADP) - if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { - if ps_type == "Mains" - || ps_type == "USB_PD_DRP" - || ps_type == "USB_PD" - || ps_type == "USB_DCP" - || ps_type == "USB_CDP" - || ps_type == "USB_ACA" - { - // USB types can also provide power - if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { - if online == 1 { - overall_ac_connected = true; - break; - } - } - } - } else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { - // Fallback for type file missing - if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { - if online == 1 { - overall_ac_connected = true; - break; - } - } - } - } - // No AC adapter detected but we're on a desktop system // Default to AC power for desktops if !overall_ac_connected { overall_ac_connected = is_likely_desktop_system(); } - - for entry in fs::read_dir(power_supply_path)? { - let entry = entry?; - let ps_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - if ignored_supplies.contains(&name) { - continue; - } - - if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { - if ps_type == "Battery" { - // Skip peripheral batteries that aren't real laptop batteries - if is_peripheral_battery(&ps_path, &name) { - log::debug!("Skipping peripheral battery: {name}"); - continue; - } - - let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok(); - let capacity_percent = read_sysfs_value::(ps_path.join("capacity")).ok(); - - let power_rate_watts = if ps_path.join("power_now").exists() { - read_sysfs_value::(ps_path.join("power_now")) // uW - .map(|uw| uw as f32 / 1_000_000.0) - .ok() - } else if ps_path.join("current_now").exists() - && ps_path.join("voltage_now").exists() - { - let current_ua = read_sysfs_value::(ps_path.join("current_now")).ok(); // uA - let voltage_uv = read_sysfs_value::(ps_path.join("voltage_now")).ok(); // uV - if let (Some(c), Some(v)) = (current_ua, voltage_uv) { - // Power (W) = (Voltage (V) * Current (A)) - // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W - Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32) - } else { - None - } - } else { - None - }; - - let charge_start_threshold = - read_sysfs_value::(ps_path.join("charge_control_start_threshold")).ok(); - let charge_stop_threshold = - read_sysfs_value::(ps_path.join("charge_control_end_threshold")).ok(); - - batteries.push(BatteryInfo { - name: name.clone(), - ac_connected: overall_ac_connected, - charging_state: status_str, - capacity_percent, - power_rate_watts, - charge_start_threshold, - charge_stop_threshold, - }); - } - } - } - // If we found no batteries but have power supplies, we're likely on a desktop if batteries.is_empty() && overall_ac_connected { log::debug!("No laptop batteries found, likely a desktop system"); @@ -366,23 +260,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> Ok(batteries) } -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); - let batteries = get_battery_info(config)?; - let system_load = get_system_load()?; - - Ok(SystemReport { - system_info, - cpu_cores, - cpu_global, - batteries, - system_load, - timestamp: SystemTime::now(), - }) -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index 5bbcebc..f213e5b 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -44,16 +44,38 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ ]; /// Represents a power supply that supports charge threshold control. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct PowerSupply { pub name: String, pub path: PathBuf, + pub type_: String, pub is_from_peripheral: bool, + pub charge_state: Option, + pub charge_percent: Option, + + pub charge_threshold_start: f64, + pub charge_threshold_end: f64, + + pub drain_rate_watts: Option, + pub threshold_config: Option, } +impl PowerSupply { + pub fn is_ac(&self) -> bool { + !self.is_from_peripheral + && matches!( + &*self.type_, + "Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA" + ) + || self.type_.starts_with("AC") + || self.type_.contains("ACAD") + || self.type_.contains("ADP") + } +} + impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "power supply '{name}'", name = self.name.yellow())?; @@ -77,6 +99,15 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, + type_: String::new(), + + charge_state: None, + charge_percent: None, + + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, + + drain_rate_watts: None, is_from_peripheral: false, @@ -99,6 +130,15 @@ impl PowerSupply { .to_string(), path, + type_: String::new(), + + charge_state: None, + charge_percent: None, + + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, + + drain_rate_watts: None, is_from_peripheral: false, @@ -131,37 +171,18 @@ impl PowerSupply { Ok(power_supplies) } - fn get_type(&self) -> anyhow::Result { - let type_path = self.path.join("type"); - - let type_ = fs::read(&type_path) - .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? - .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - - Ok(type_) - } - pub fn rescan(&mut self) -> anyhow::Result<()> { if !self.path.exists() { bail!("{self} does not exist"); } - self.threshold_config = self - .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() - && self.path.join(config.path_end).exists() - { - return Some(*config); - } - } + self.type_ = { + let type_path = self.path.join("type"); - None - }) - .flatten(); + fs::read(&type_path) + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))? + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? + }; self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); @@ -179,10 +200,9 @@ impl PowerSupply { } // Small capacity batteries are likely not laptop batteries. - if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { - let energy_full = energy_full - .with_context(|| format!("failed to read the max charge '{self}' can hold"))?; - + if let Some(energy_full) = fs::read_n::(self.path.join("energy_full")) + .with_context(|| format!("failed to read the max charge {self} can hold"))? + { // Most laptop batteries are at least 20,000,000 µWh (20 Wh). // Peripheral batteries are typically much smaller. if energy_full < 10_000_000 { @@ -191,10 +211,9 @@ impl PowerSupply { } } // Check for model name that indicates a peripheral - if let Some(model) = fs::read(self.path.join("model_name")) { - let model = - model.with_context(|| format!("failed to read the model name of '{self}'"))?; - + if let Some(model) = fs::read(self.path.join("model_name")) + .with_context(|| format!("failed to read the model name of {self}"))? + { if model.contains("bluetooth") || model.contains("wireless") { break 'is_from_peripheral true; } @@ -203,6 +222,53 @@ impl PowerSupply { false }; + if self.type_ == "Battery" { + self.charge_state = fs::read(self.path.join("status")) + .with_context(|| format!("failed to read {self} charge status"))?; + + self.charge_percent = fs::read_n::(self.path.join("capacity")) + .with_context(|| format!("failed to read {self} charge percent"))? + .map(|percent| percent as f64 / 100.0); + + self.charge_threshold_start = + fs::read_n::(self.path.join("charge_control_start_threshold")) + .with_context(|| format!("failed to read {self} charge threshold start"))? + .map_or(0.0, |percent| percent as f64 / 100.0); + + self.charge_threshold_end = + fs::read_n::(self.path.join("charge_control_end_threshold")) + .with_context(|| format!("failed to read {self} charge threshold end"))? + .map_or(100.0, |percent| percent as f64 / 100.0); + + self.drain_rate_watts = match fs::read_n::(self.path.join("power_now")) + .with_context(|| format!("failed to read {self} power drain"))? + { + Some(drain) => Some(drain as f64), + + None => { + let current_ua = fs::read_n::(self.path.join("current_now")) + .with_context(|| format!("failed to read {self} current"))?; + + let voltage_uv = fs::read_n::(self.path.join("voltage_now")) + .with_context(|| format!("failed to read {self} voltage"))?; + + current_ua.zip(voltage_uv).map(|(current, voltage)| { + // Power (W) = Voltage (V) * Current (A) + // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W + current as f64 * voltage as f64 / 1e12 + }) + } + }; + + self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS + .iter() + .find(|config| { + self.path.join(config.path_start).exists() + && self.path.join(config.path_end).exists() + }) + .copied(); + } + Ok(()) } @@ -216,7 +282,10 @@ impl PowerSupply { .map(|config| self.path.join(config.path_end)) } - pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { + pub fn set_charge_threshold_start( + &mut self, + charge_threshold_start: f64, + ) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_start().ok_or_else(|| { anyhow!( @@ -224,16 +293,18 @@ impl PowerSupply { name = self.name, ) })?, - &charge_threshold_start.to_string(), + &((charge_threshold_start * 100.0) as u8).to_string(), ) .with_context(|| format!("failed to set charge threshold start for {self}"))?; + self.charge_threshold_start = charge_threshold_start; + log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); Ok(()) } - pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { + pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_end().ok_or_else(|| { anyhow!( @@ -241,26 +312,30 @@ impl PowerSupply { name = self.name, ) })?, - &charge_threshold_end.to_string(), + &((charge_threshold_end * 100.0) as u8).to_string(), ) .with_context(|| format!("failed to set charge threshold end for {self}"))?; + self.charge_threshold_end = charge_threshold_end; + log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); Ok(()) } - pub fn get_available_platform_profiles() -> Vec { + pub fn get_available_platform_profiles() -> anyhow::Result> { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Some(Ok(content)) = fs::read(path) else { - return Vec::new(); + let Some(content) = + fs::read(path).context("failed to read available ACPI platform profiles")? + else { + return Ok(Vec::new()); }; - content + Ok(content .split_whitespace() .map(ToString::to_string) - .collect() + .collect()) } /// Sets the platform profile. @@ -270,7 +345,7 @@ impl PowerSupply { /// /// [`The Kernel docs`]: pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { - let profiles = Self::get_available_platform_profiles(); + let profiles = Self::get_available_platform_profiles()?; if !profiles .iter() @@ -285,4 +360,10 @@ impl PowerSupply { fs::write("/sys/firmware/acpi/platform_profile", profile) .context("this probably means that your system does not support changing ACPI profiles") } + + pub fn platform_profile() -> anyhow::Result { + fs::read("/sys/firmware/acpi/platform_profile") + .context("failed to read platform profile")? + .context("failed to find platform profile") + } } diff --git a/src/system.rs b/src/system.rs index f8820b1..5781c3c 100644 --- a/src/system.rs +++ b/src/system.rs @@ -33,9 +33,9 @@ impl System { } fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { - if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { - let chassis_type = chassis_type.context("failed to read chassis type")?; - + if let Some(chassis_type) = + fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? + { // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis @@ -86,8 +86,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("load average file doesn't exist, are you on linux?")? - .context("failed to read load average")?; + .context("failed to read load average")? + .context("load average file doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace(); From 533825beb8555da67560179f254380ebdbcf00b3 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 20:12:25 +0300 Subject: [PATCH 35/93] system: is_ac --- src/monitor.rs | 14 -------------- src/system.rs | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/monitor.rs b/src/monitor.rs index 19cc69e..88195c8 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -246,20 +246,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { } } -pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { - // No AC adapter detected but we're on a desktop system - // Default to AC power for desktops - if !overall_ac_connected { - overall_ac_connected = is_likely_desktop_system(); - } - // If we found no batteries but have power supplies, we're likely on a desktop - if batteries.is_empty() && overall_ac_connected { - log::debug!("No laptop batteries found, likely a desktop system"); - } - - Ok(batteries) -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/system.rs b/src/system.rs index 5781c3c..17f8b73 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,19 +1,25 @@ use anyhow::{Context, bail}; -use crate::fs; +use crate::{cpu, fs, power_supply}; pub struct System { - pub is_desktop: bool, + pub is_ac: bool, pub load_average_1min: f64, pub load_average_5min: f64, pub load_average_15min: f64, + + pub cpus: Vec, + pub power_supplies: Vec, } impl System { pub fn new() -> anyhow::Result { let mut system = Self { - is_desktop: false, + is_ac: false, + + cpus: Vec::new(), + power_supplies: Vec::new(), load_average_1min: 0.0, load_average_5min: 0.0, @@ -26,13 +32,23 @@ impl System { } pub fn rescan(&mut self) -> anyhow::Result<()> { - self.rescan_is_desktop()?; + self.cpus = cpu::Cpu::all().context("failed to scan CPUs")?; + + self.power_supplies = + power_supply::PowerSupply::all().context("failed to scan power supplies")?; + + self.is_ac = self + .power_supplies + .iter() + .any(|power_supply| power_supply.is_ac()) + || self.is_desktop()?; + self.rescan_load_average()?; Ok(()) } - fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { + fn is_desktop(&mut self) -> anyhow::Result { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? { @@ -42,13 +58,11 @@ impl System { match chassis_type.trim() { // Desktop form factors. "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { - self.is_desktop = true; - return Ok(()); + return Ok(true); } // Laptop form factors. "9" | "10" | "14" => { - self.is_desktop = false; - return Ok(()); + return Ok(false); } // Unknown, continue with other checks @@ -61,8 +75,7 @@ impl System { || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); if !power_saving_exists { - self.is_desktop = true; - return Ok(()); // Likely a desktop. + return Ok(true); // Likely a desktop. } // Check battery-specific ACPI paths that laptops typically have @@ -74,14 +87,12 @@ impl System { for path in laptop_acpi_paths { if fs::exists(path) { - self.is_desktop = false; // Likely a laptop. - return Ok(()); + return Ok(false); // Likely a laptop. } } // Default to assuming desktop if we can't determine. - self.is_desktop = true; - Ok(()) + Ok(true) } fn rescan_load_average(&mut self) -> anyhow::Result<()> { From 15bcdd269cf9208ed756f1ce07ad6a1fca926158 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 27 May 2025 21:24:06 +0300 Subject: [PATCH 36/93] system: check for chassis type 31 and move power saving check below Co-Authored-By: flashrun24 --- src/system.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/system.rs b/src/system.rs index 17f8b73..02b7b80 100644 --- a/src/system.rs +++ b/src/system.rs @@ -52,16 +52,18 @@ impl System { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? { - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One - // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower, + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One, + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis, + // 31=Convertible Laptop match chassis_type.trim() { // Desktop form factors. "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { return Ok(true); } + // Laptop form factors. - "9" | "10" | "14" => { + "9" | "10" | "14" | "31" => { return Ok(false); } @@ -70,14 +72,6 @@ impl System { } } - // Check CPU power policies, desktops often don't have these - let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") - || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); - - if !power_saving_exists { - return Ok(true); // Likely a desktop. - } - // Check battery-specific ACPI paths that laptops typically have let laptop_acpi_paths = [ "/sys/class/power_supply/BAT0", @@ -91,6 +85,14 @@ impl System { } } + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + return Ok(true); // Likely a desktop. + } + // Default to assuming desktop if we can't determine. Ok(true) } From 6b4972864f4ec46a0129f6fd8b4f71693472b738 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 28 May 2025 21:47:44 +0300 Subject: [PATCH 37/93] cpu: cache /proc/stat --- src/config.rs | 3 +- src/cpu.rs | 169 +++++++++++++++++++++++++------------------------- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4de0ac3..2560806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,9 +53,10 @@ impl CpuDelta { let mut cpus = match &self.for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); + let cache = cpu::CpuRescanCache::default(); for &number in numbers { - cpus.push(cpu::Cpu::new(number)?); + cpus.push(cpu::Cpu::new(number, &cache)?); } cpus diff --git a/src/cpu.rs b/src/cpu.rs index 6712cdf..4979b67 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,10 +1,27 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{fmt, string::ToString}; +use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; use crate::fs; +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct CpuRescanCache { + stat: OnceCell>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CpuStat { + pub user: u64, + pub nice: u64, + pub system: u64, + pub idle: u64, + pub iowait: u64, + pub irq: u64, + pub softirq: u64, + pub steal: u64, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cpu { pub number: u32, @@ -24,30 +41,23 @@ pub struct Cpu { pub available_epbs: Vec, pub epb: Option, - pub time_user: u64, - pub time_nice: u64, - pub time_system: u64, - pub time_idle: u64, - pub time_iowait: u64, - pub time_irq: u64, - pub time_softirq: u64, - pub time_steal: u64, + pub stat: CpuStat, } impl Cpu { pub fn time_total(&self) -> u64 { - self.time_user - + self.time_nice - + self.time_system - + self.time_idle - + self.time_iowait - + self.time_irq - + self.time_softirq - + self.time_steal + self.stat.user + + self.stat.nice + + self.stat.system + + self.stat.idle + + self.stat.iowait + + self.stat.irq + + self.stat.softirq + + self.stat.steal } pub fn time_idle(&self) -> u64 { - self.time_idle + self.time_iowait + self.stat.idle + self.stat.iowait } pub fn usage(&self) -> f64 { @@ -64,7 +74,7 @@ impl fmt::Display for Cpu { } impl Cpu { - pub fn new(number: u32) -> anyhow::Result { + pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result { let mut cpu = Self { number, has_cpufreq: false, @@ -82,16 +92,18 @@ impl Cpu { available_epbs: Vec::new(), epb: None, - time_user: 0, - time_nice: 0, - time_system: 0, - time_idle: 0, - time_iowait: 0, - time_irq: 0, - time_softirq: 0, - time_steal: 0, + stat: CpuStat { + user: 0, + nice: 0, + system: 0, + idle: 0, + iowait: 0, + irq: 0, + softirq: 0, + steal: 0, + }, }; - cpu.rescan()?; + cpu.rescan(cache)?; Ok(cpu) } @@ -101,6 +113,7 @@ impl Cpu { const PATH: &str = "/sys/devices/system/cpu"; let mut cpus = vec![]; + let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) .with_context(|| format!("failed to read contents of '{PATH}'"))? @@ -121,13 +134,13 @@ impl Cpu { continue; }; - cpus.push(Self::new(number)?); + cpus.push(Self::new(number, &cache)?); } // 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)?); + cpus.push(Self::new(number, &cache)?); } } @@ -135,7 +148,7 @@ impl Cpu { } /// Rescan CPU, tuning local copy of settings. - pub fn rescan(&mut self) -> anyhow::Result<()> { + pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { let Self { number, .. } = self; if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { @@ -144,7 +157,7 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.rescan_times()?; + self.rescan_stat(cache)?; if self.has_cpufreq { self.rescan_governor()?; @@ -156,63 +169,47 @@ impl Cpu { Ok(()) } - fn rescan_times(&mut self) -> anyhow::Result<()> { - // TODO: Don't read this per CPU. Share the read or - // find something in /sys/.../cpu{N} that does it. - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, - let cpu_name = format!("cpu{number}", number = self.number); + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; - let mut stats = content - .lines() - .find_map(|line| { - line.starts_with(&cpu_name) - .then(|| line.split_whitespace().skip(1)) - }) - .with_context(|| format!("failed to find {self} in CPU stats"))?; + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - self.time_user = stats - .next() - .with_context(|| format!("failed to parse {self} user time"))? - .parse() - .with_context(|| format!("failed to find {self} user time"))?; - self.time_nice = stats - .next() - .with_context(|| format!("failed to parse {self} nice time"))? - .parse() - .with_context(|| format!("failed to find {self} nice time"))?; - self.time_system = stats - .next() - .with_context(|| format!("failed to parse {self} system time"))? - .parse() - .with_context(|| format!("failed to find {self} system time"))?; - self.time_idle = stats - .next() - .with_context(|| format!("failed to parse {self} idle time"))? - .parse() - .with_context(|| format!("failed to find {self} idle time"))?; - self.time_iowait = stats - .next() - .with_context(|| format!("failed to parse {self} iowait time"))? - .parse() - .with_context(|| format!("failed to find {self} iowait time"))?; - self.time_irq = stats - .next() - .with_context(|| format!("failed to parse {self} irq time"))? - .parse() - .with_context(|| format!("failed to find {self} irq time"))?; - self.time_softirq = stats - .next() - .with_context(|| format!("failed to parse {self} softirq time"))? - .parse() - .with_context(|| format!("failed to find {self} softirq time"))?; - self.time_steal = stats - .next() - .with_context(|| format!("failed to parse {self} steal time"))? - .parse() - .with_context(|| format!("failed to find {self} steal time"))?; + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); Ok(()) } From 2dbc7478686543222255a4f10c3e1403eb7da5f8 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 28 May 2025 22:35:31 +0300 Subject: [PATCH 38/93] cpu: wip temperature scanning, waiting for raf to stand up from his desk and open his laptop on the other side of the room --- src/cpu.rs | 203 +++++++++++++++++++++++++++++--------------- src/fs.rs | 18 ++-- src/monitor.rs | 65 +------------- src/power_supply.rs | 1 + src/system.rs | 4 +- 5 files changed, 149 insertions(+), 142 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 4979b67..3c0f332 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,9 +5,10 @@ use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; use crate::fs; -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, + temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -22,7 +23,28 @@ pub struct CpuStat { pub steal: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +impl CpuStat { + pub fn total(&self) -> u64 { + self.user + + self.nice + + self.system + + self.idle + + self.iowait + + self.irq + + self.softirq + + self.steal + } + + pub fn idle(&self) -> u64 { + self.idle + self.iowait + } + + pub fn usage(&self) -> f64 { + 1.0 - self.idle() as f64 / self.total() as f64 + } +} + +#[derive(Debug, Clone, PartialEq)] pub struct Cpu { pub number: u32, @@ -42,27 +64,8 @@ pub struct Cpu { pub epb: Option, pub stat: CpuStat, -} -impl Cpu { - pub fn time_total(&self) -> u64 { - self.stat.user - + self.stat.nice - + self.stat.system - + self.stat.idle - + self.stat.iowait - + self.stat.irq - + self.stat.softirq - + self.stat.steal - } - - pub fn time_idle(&self) -> u64 { - self.stat.idle + self.stat.iowait - } - - pub fn usage(&self) -> f64 { - 1.0 - self.time_idle() as f64 / self.time_total() as f64 - } + pub temperature: Option, } impl fmt::Display for Cpu { @@ -102,6 +105,8 @@ impl Cpu { softirq: 0, steal: 0, }, + + temperature: None, }; cpu.rescan(cache)?; @@ -116,9 +121,11 @@ impl Cpu { let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read contents of '{PATH}'"))? - .flatten() + .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + let entry_file_name = entry.file_name(); let Some(name) = entry_file_name.to_str() else { @@ -157,8 +164,6 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.rescan_stat(cache)?; - if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; @@ -166,50 +171,8 @@ impl Cpu { self.rescan_epb()?; } - Ok(()) - } - - fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let stat = match cache.stat.get() { - Some(stat) => stat, - - None => { - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; - - cache - .stat - .set(HashMap::from_iter(content.lines().skip(1).filter_map( - |line| { - let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - - let number = parts.next()?.parse().ok()?; - - let stat = CpuStat { - user: parts.next()?.parse().ok()?, - nice: parts.next()?.parse().ok()?, - system: parts.next()?.parse().ok()?, - idle: parts.next()?.parse().ok()?, - iowait: parts.next()?.parse().ok()?, - irq: parts.next()?.parse().ok()?, - softirq: parts.next()?.parse().ok()?, - steal: parts.next()?.parse().ok()?, - }; - - Some((number, stat)) - }, - ))); - - cache.stat.get().unwrap() - } - }; - - self.stat = stat - .get(&self.number) - .with_context(|| format!("failed to get stat of {self}"))? - .clone(); + self.rescan_stat(cache)?; + self.rescan_temperature(cache)?; Ok(()) } @@ -338,6 +301,106 @@ impl Cpu { Ok(()) } + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, + + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; + + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); + + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))) + .unwrap(); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); + + Ok(()) + } + + fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let temperatures = match cache.temperatures.get() { + Some(temperature) => temperature, + + None => { + const PATH: &str = "/sys/class/hwmon"; + + let temperatures = HashMap::new(); + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = + entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; + + match &*name { + // Intel CPU temperature driver + "coretemp" => todo!(), + + // AMD CPU temperature driver + // TODO: 'zenergy' can also report those stats, I think? + "k10temp" | "zenpower" | "amdgpu" => todo!(), + + // Other CPU temperature drivers + _ if name.contains("cpu") || name.contains("temp") => todo!(), + + _ => {} + } + } + + cache.temperatures.set(temperatures).unwrap(); + cache.temperatures.get().unwrap() + } + }; + + self.temperature = temperatures.get(&self.number).copied(); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/fs.rs b/src/fs.rs index 526856d..3192e4d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -8,11 +8,19 @@ pub fn exists(path: impl AsRef) -> bool { path.exists() } -pub fn read_dir(path: impl AsRef) -> anyhow::Result { +pub fn read_dir(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); - fs::read_dir(path) - .with_context(|| format!("failed to read directory '{path}'", path = path.display())) + match fs::read_dir(path) { + Ok(entries) => Ok(Some(entries)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => Err(error).context(format!( + "failed to read directory '{path}'", + path = path.display() + )), + } } pub fn read(path: impl AsRef) -> anyhow::Result> { @@ -23,9 +31,7 @@ pub fn read(path: impl AsRef) -> anyhow::Result> { Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), } } diff --git a/src/monitor.rs b/src/monitor.rs index 88195c8..d4534ba 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -16,11 +16,7 @@ pub fn get_system_info() -> SystemInfo { SystemInfo { cpu_model } } -pub fn get_cpu_core_info( - core_id: u32, - prev_times: &CpuTimes, - current_times: &CpuTimes, -) -> anyhow::Result { +pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -187,65 +183,6 @@ fn get_fallback_temperature(hw_path: &Path) -> Option { None } -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()?; - - 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); - - for core_id in 0..num_cores { - if let (Some(prev), Some(curr)) = ( - initial_cpu_times.get(&core_id), - final_cpu_times.get(&core_id), - ) { - match get_cpu_core_info(core_id, prev, curr) { - Ok(info) => core_infos.push(info), - Err(e) => { - // Log or handle error for a single core, maybe push a partial info or skip - eprintln!("Error getting info for core {core_id}: {e}"); - } - } - } else { - // Log or handle missing times for a core - eprintln!("Missing CPU time data for core {core_id}"); - } - } - Ok(core_infos) -} - -pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - // Calculate average CPU temperature from the core temperatures - let average_temperature_celsius = if cpu_cores.is_empty() { - None - } else { - // Filter cores with temperature readings, then calculate average - let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores - .iter() - .filter(|core| core.temperature_celsius.is_some()) - .collect(); - - if cores_with_temp.is_empty() { - None - } else { - // Sum up all temperatures and divide by count - let sum: f32 = cores_with_temp - .iter() - .map(|core| core.temperature_celsius.unwrap()) - .sum(); - Some(sum / cores_with_temp.len() as f32) - } - }; - - // Return the constructed CpuGlobalInfo - CpuGlobalInfo { - average_temperature_celsius, - } -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index f213e5b..2155c29 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -155,6 +155,7 @@ impl PowerSupply { for entry in fs::read_dir(POWER_SUPPLY_PATH) .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + .with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? { let entry = match entry { Ok(entry) => entry, diff --git a/src/system.rs b/src/system.rs index 02b7b80..57d5ce2 100644 --- a/src/system.rs +++ b/src/system.rs @@ -99,8 +99,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("failed to read load average")? - .context("load average file doesn't exist, are you on linux?")?; + .context("failed to read load average from '/proc/loadavg'")? + .context("'/proc/loadavg' doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace(); From 7b375439bb04b67ceb2cbaa3983eb01ce746f444 Mon Sep 17 00:00:00 2001 From: Jacob Birkett Date: Sat, 31 May 2025 12:19:50 -0700 Subject: [PATCH 39/93] flake: formatter: set to alejandra --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index affc70b..ab6d122 100644 --- a/flake.nix +++ b/flake.nix @@ -22,5 +22,7 @@ superfreq = import ./nix/module.nix inputs; default = self.nixosModules.superfreq; }; + + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra); }; } From 6b1af5cbabef28541748bfc88c8ff1e7b9751d65 Mon Sep 17 00:00:00 2001 From: Jacob Birkett Date: Sat, 31 May 2025 12:25:28 -0700 Subject: [PATCH 40/93] flake: pkgsForEach: replace legacyPackages with manual import --- flake.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index affc70b..135a05a 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,10 @@ ... } @ inputs: let forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"]; - pkgsForEach = nixpkgs.legacyPackages; + pkgsForEach = forAllSystems (system: + import nixpkgs { + localSystem.system = system; + }); in { packages = forAllSystems (system: { superfreq = pkgsForEach.${system}.callPackage ./nix/package.nix {}; From 08c51b6296d868c77724b4ccec091e75f42a999a Mon Sep 17 00:00:00 2001 From: Jacob Birkett Date: Sat, 31 May 2025 12:27:32 -0700 Subject: [PATCH 41/93] flake: overlays: add superfreq and default --- flake.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flake.nix b/flake.nix index 135a05a..8318810 100644 --- a/flake.nix +++ b/flake.nix @@ -12,6 +12,13 @@ localSystem.system = system; }); in { + overlays = { + superfreq = final: _: { + superfreq = final.callPackage ./nix/package.nix {}; + }; + default = self.overlays.superfreq; + }; + packages = forAllSystems (system: { superfreq = pkgsForEach.${system}.callPackage ./nix/package.nix {}; default = self.packages.${system}.superfreq; From 3caaa22f3efeba6e3cf3a2a204aa015fb4264394 Mon Sep 17 00:00:00 2001 From: Jacob Birkett Date: Sat, 31 May 2025 12:43:55 -0700 Subject: [PATCH 42/93] flake: packages: inherit from default overlay --- flake.nix | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 8318810..4c012bb 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,7 @@ pkgsForEach = forAllSystems (system: import nixpkgs { localSystem.system = system; + overlays = [self.overlays.default]; }); in { overlays = { @@ -19,10 +20,12 @@ default = self.overlays.superfreq; }; - packages = forAllSystems (system: { - superfreq = pkgsForEach.${system}.callPackage ./nix/package.nix {}; - default = self.packages.${system}.superfreq; - }); + packages = + nixpkgs.lib.mapAttrs (system: pkgs: { + inherit (pkgs) superfreq; + default = self.packages.${system}.superfreq; + }) + pkgsForEach; devShells = forAllSystems (system: { default = pkgsForEach.${system}.callPackage ./nix/shell.nix {}; From 0f3d5d81dd96a25642ca7239a17843d5e9639217 Mon Sep 17 00:00:00 2001 From: Jacob Birkett Date: Sat, 31 May 2025 13:11:25 -0700 Subject: [PATCH 43/93] flake: devShells: use same pkgs as for packages output --- flake.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 4c012bb..96f329d 100644 --- a/flake.nix +++ b/flake.nix @@ -27,9 +27,11 @@ }) pkgsForEach; - devShells = forAllSystems (system: { - default = pkgsForEach.${system}.callPackage ./nix/shell.nix {}; - }); + devShells = + nixpkgs.lib.mapAttrs (system: pkgs: { + default = pkgs.callPackage ./nix/shell.nix {}; + }) + pkgsForEach; nixosModules = { superfreq = import ./nix/module.nix inputs; From 0358e0bece1b43b25fb27bbde12ea09f52f751a6 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 18:34:40 +0300 Subject: [PATCH 44/93] system: cpu temperatures scanning --- src/cpu.rs | 56 -------------- src/monitor.rs | 194 ++++++------------------------------------------- src/system.rs | 110 ++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 227 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 3c0f332..7a60ee3 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -8,7 +8,6 @@ use crate::fs; #[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, - temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -172,7 +171,6 @@ impl Cpu { } self.rescan_stat(cache)?; - self.rescan_temperature(cache)?; Ok(()) } @@ -347,60 +345,6 @@ impl Cpu { Ok(()) } - fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let temperatures = match cache.temperatures.get() { - Some(temperature) => temperature, - - None => { - const PATH: &str = "/sys/class/hwmon"; - - let temperatures = HashMap::new(); - - for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read hardware information from '{PATH}'"))? - .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? - { - let entry = - entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - - let entry_path = entry.path(); - - let Some(name) = fs::read(entry_path.join("name")).with_context(|| { - format!( - "failed to read name of hardware entry at '{path}'", - path = entry_path.display(), - ) - })? - else { - continue; - }; - - match &*name { - // Intel CPU temperature driver - "coretemp" => todo!(), - - // AMD CPU temperature driver - // TODO: 'zenergy' can also report those stats, I think? - "k10temp" | "zenpower" | "amdgpu" => todo!(), - - // Other CPU temperature drivers - _ if name.contains("cpu") || name.contains("temp") => todo!(), - - _ => {} - } - } - - cache.temperatures.set(temperatures).unwrap(); - cache.temperatures.get().unwrap() - } - }; - - self.temperature = temperatures.get(&self.number).copied(); - - Ok(()) - } - pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/monitor.rs b/src/monitor.rs index d4534ba..40b0242 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -10,178 +10,30 @@ use std::{ time::SystemTime, }; -pub fn get_system_info() -> SystemInfo { - let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string()); +// Try /sys/devices/platform paths for thermal zones as a last resort +// if temperature_celsius.is_none() { +// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { +// for entry in thermal_zones.flatten() { +// let zone_path = entry.path(); +// let name = entry.file_name().into_string().unwrap_or_default(); - SystemInfo { cpu_model } -} - -pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { - // Temperature detection. - // Should be generic enough to be able to support for multiple hardware sensors - // with the possibility of extending later down the road. - let mut temperature_celsius: Option = None; - - // Search for temperature in hwmon devices - if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") { - for hw_entry in hwmon_dir.flatten() { - let hw_path = hw_entry.path(); - - // Check hwmon driver name - if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) { - // Intel CPU temperature driver - if name == "coretemp" { - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - } - // AMD CPU temperature driver - // TODO: 'zenergy' can also report those stats, I think? - else if name == "k10temp" || name == "zenpower" || name == "amdgpu" { - // AMD's k10temp doesn't always label cores individually - // First try to find core-specific temps - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") { - temperature_celsius = Some(temp); - break; - } - - // Try Tctl temperature (CPU control temp) - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "Tctl") { - temperature_celsius = Some(temp); - break; - } - - // Try CPU temperature - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "CPU") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any available temperature input without a specific label - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - // Other CPU temperature drivers - else if name.contains("cpu") || name.contains("temp") { - // Try to find a label that matches this core - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any temperature reading if specific core not found - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - } - } - } - - // Try /sys/devices/platform paths for thermal zones as a last resort - if temperature_celsius.is_none() { - if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { - for entry in thermal_zones.flatten() { - let zone_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - if name.starts_with("thermal_zone") { - // Try to match by type - if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { - if zone_type.contains("cpu") - || zone_type.contains("x86") - || zone_type.contains("core") - { - if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { - temperature_celsius = Some(temp_mc as f32 / 1000.0); - break; - } - } - } - } - } - } - } - - Ok(CpuCoreInfo { - core_id, - temperature_celsius, - }) -} - -/// Finds core-specific temperature -fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option { - for i in 1..=32 { - // Increased range to handle systems with many sensors - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - // Match various common label formats: - // "Core X", "core X", "Core-X", "CPU Core X", etc. - let core_pattern = format!("{label_prefix} {core_id}"); - let alt_pattern = format!("{label_prefix}-{core_id}"); - - if label.eq_ignore_ascii_case(&core_pattern) - || label.eq_ignore_ascii_case(&alt_pattern) - || label - .to_lowercase() - .contains(&format!("core {core_id}").to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } - } - None -} - -// Finds generic sensor temperatures by label -fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option { - for i in 1..=32 { - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - if label.eq_ignore_ascii_case(label_name) - || label.to_lowercase().contains(&label_name.to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } else if !label_path.exists() && input_path.exists() { - // Some sensors might not have labels but still have valid temp inputs - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} - -// Fallback to any temperature reading from a sensor -fn get_fallback_temperature(hw_path: &Path) -> Option { - for i in 1..=32 { - let input_path = hw_path.join(format!("temp{i}_input")); - - if input_path.exists() { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} +// if name.starts_with("thermal_zone") { +// // Try to match by type +// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { +// if zone_type.contains("cpu") +// || zone_type.contains("x86") +// || zone_type.contains("core") +// { +// if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { +// temperature_celsius = Some(temp_mc as f32 / 1000.0); +// break; +// } +// } +// } +// } +// } +// } +// } pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); diff --git a/src/system.rs b/src/system.rs index 57d5ce2..4a86893 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, path::Path}; + use anyhow::{Context, bail}; use crate::{cpu, fs, power_supply}; @@ -10,6 +12,8 @@ pub struct System { pub load_average_15min: f64, pub cpus: Vec, + pub cpu_temperatures: HashMap, + pub power_supplies: Vec, } @@ -19,6 +23,8 @@ impl System { is_ac: false, cpus: Vec::new(), + cpu_temperatures: HashMap::new(), + power_supplies: Vec::new(), load_average_1min: 0.0, @@ -48,6 +54,110 @@ impl System { Ok(()) } + fn rescan_temperatures(&mut self) -> anyhow::Result<()> { + const PATH: &str = "/sys/class/hwmon"; + + let mut temperatures = HashMap::new(); + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; + + match &*name { + // TODO: 'zenergy' can also report those stats, I think? + "coretemp" | "k10temp" | "zenpower" | "amdgpu" => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + // Other CPU temperature drivers. + _ if name.contains("cpu") || name.contains("temp") => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + _ => {} + } + } + + self.cpu_temperatures = temperatures; + + Ok(()) + } + + fn get_temperatures( + device_path: &Path, + temperatures: &mut HashMap, + ) -> anyhow::Result<()> { + // Increased range to handle systems with many sensors. + for i in 1..=96 { + let label_path = device_path.join(format!("temp{i}_label")); + let input_path = device_path.join(format!("temp{i}_input")); + + if !label_path.exists() || !input_path.exists() { + continue; + } + + let Some(label) = fs::read(&label_path).with_context(|| { + format!( + "failed to read hardware hardware device label from '{path}'", + path = label_path.display(), + ) + })? + else { + continue; + }; + + // Match various common label formats: + // "Core X", "core X", "Core-X", "CPU Core X", etc. + let number = label + .trim_start_matches("cpu") + .trim_start_matches("CPU") + .trim_start() + .trim_start_matches("core") + .trim_start_matches("Core") + .trim_start() + .trim_start_matches("tdie") + .trim_start_matches("Tdie") + .trim_start() + .trim_start_matches("tctl") + .trim_start_matches("Tctl") + .trim_start() + .trim_start_matches("-") + .trim(); + + let Ok(number) = number.parse::() else { + continue; + }; + + let Some(temperature_mc) = fs::read_n::(&input_path).with_context(|| { + format!( + "failed to read CPU temperature from '{path}'", + path = input_path.display(), + ) + })? + else { + continue; + }; + + temperatures.insert(number, temperature_mc as f64 / 1000.0); + } + + Ok(()) + } + fn is_desktop(&mut self) -> anyhow::Result { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? From 87085f913b50372a82455bf312ee4a6cd2e94c61 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 17:26:48 +0300 Subject: [PATCH 45/93] cpu: clean up, clean main too --- Cargo.lock | 62 ++++ Cargo.toml | 3 + src/battery.rs | 31 +- src/core.rs | 28 -- src/cpu.rs | 815 ++++++++++++++++++---------------------------- src/daemon.rs | 71 ++-- src/engine.rs | 68 ++-- src/main.rs | 869 +++++++++++++++++++++++++------------------------ src/monitor.rs | 11 +- 9 files changed, 904 insertions(+), 1054 deletions(-) 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) From d0932ae78cdc14f6f1dbbec7e6153b8013c8853b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:12:18 +0300 Subject: [PATCH 46/93] battery: clean up, rename to power_supply --- src/battery.rs | 267 -------------------------------------------- src/config/types.rs | 12 +- src/engine.rs | 4 +- src/main.rs | 27 ++--- src/power_supply.rs | 175 +++++++++++++++++++++++++++++ 5 files changed, 190 insertions(+), 295 deletions(-) delete mode 100644 src/battery.rs create mode 100644 src/power_supply.rs diff --git a/src/battery.rs b/src/battery.rs deleted file mode 100644 index b15e6bb..0000000 --- a/src/battery.rs +++ /dev/null @@ -1,267 +0,0 @@ -use crate::{config::types::BatteryChargeThresholds, util::error::ControlError, util::sysfs}; -use std::{ - fs, io, - path::{Path, PathBuf}, -}; - -pub type Result = std::result::Result; - -/// Represents a pattern of path suffixes used to control battery charge thresholds -/// for different device vendors. -#[derive(Clone)] -pub struct ThresholdPathPattern { - pub description: &'static str, - pub start_path: &'static str, - pub stop_path: &'static str, -} - -// Threshold patterns -const THRESHOLD_PATTERNS: &[ThresholdPathPattern] = &[ - ThresholdPathPattern { - description: "Standard", - start_path: "charge_control_start_threshold", - stop_path: "charge_control_end_threshold", - }, - ThresholdPathPattern { - description: "ASUS", - start_path: "charge_control_start_percentage", - stop_path: "charge_control_end_percentage", - }, - // Combine Huawei and ThinkPad since they use identical paths - ThresholdPathPattern { - description: "ThinkPad/Huawei", - start_path: "charge_start_threshold", - stop_path: "charge_stop_threshold", - }, - // Framework laptop support - ThresholdPathPattern { - description: "Framework", - start_path: "charge_behaviour_start_threshold", - stop_path: "charge_behaviour_end_threshold", - }, -]; - -/// Represents a battery that supports charge threshold control -pub struct SupportedBattery<'a> { - pub name: String, - pub pattern: &'a ThresholdPathPattern, - pub path: PathBuf, -} - -/// Set battery charge thresholds to protect battery health -/// -/// This sets the start and stop charging thresholds for batteries that support this feature. -/// Different laptop vendors implement battery thresholds in different ways, so this function -/// attempts to handle multiple implementations (Lenovo, ASUS, etc.). -/// -/// The thresholds determine at what percentage the battery starts charging (when below `start_threshold`) -/// and at what percentage it stops (when it reaches `stop_threshold`). -/// -/// # Arguments -/// -/// * `start_threshold` - The battery percentage at which charging should start (typically 0-99) -/// * `stop_threshold` - The battery percentage at which charging should stop (typically 1-100) -/// -/// # Errors -/// -/// Returns an error if: -/// - The thresholds are invalid (start >= stop or stop > 100) -/// - No power supply path is found -/// - No batteries with threshold support are found -/// - Failed to set thresholds on any battery -pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) -> Result<()> { - // Validate thresholds using `BatteryChargeThresholds` - let thresholds = - BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e { - crate::config::types::ConfigError::Validation(msg) => { - ControlError::InvalidValueError(msg) - } - _ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")), - })?; - - let power_supply_path = Path::new("/sys/class/power_supply"); - if !power_supply_path.exists() { - return Err(ControlError::NotSupported( - "Power supply path not found, battery threshold control not supported".to_string(), - )); - } - - // XXX: Skip checking directory writability since /sys is a virtual filesystem - // Individual file writability will be checked by find_battery_with_threshold_support - - let supported_batteries = find_supported_batteries(power_supply_path)?; - if supported_batteries.is_empty() { - return Err(ControlError::NotSupported( - "No batteries with charge threshold control support found".to_string(), - )); - } - - apply_thresholds_to_batteries(&supported_batteries, thresholds.start, thresholds.stop) -} - -/// Finds all batteries in the system that support threshold control -fn find_supported_batteries(power_supply_path: &Path) -> Result>> { - let entries = fs::read_dir(power_supply_path).map_err(|e| { - if e.kind() == io::ErrorKind::PermissionDenied { - ControlError::PermissionDenied(format!( - "Permission denied accessing power supply directory: {}", - power_supply_path.display() - )) - } else { - ControlError::Io(e) - } - })?; - - let mut supported_batteries = Vec::new(); - for entry in entries { - let entry = match entry { - Ok(e) => e, - Err(e) => { - log::warn!("Failed to read power-supply entry: {e}"); - continue; - } - }; - let ps_path = entry.path(); - if is_battery(&ps_path)? { - if let Some(battery) = find_battery_with_threshold_support(&ps_path) { - supported_batteries.push(battery); - } - } - } - - if supported_batteries.is_empty() { - log::warn!("No batteries with charge threshold support found"); - } else { - log::debug!( - "Found {} batteries with threshold support", - supported_batteries.len() - ); - for battery in &supported_batteries { - log::debug!( - "Battery '{}' supports {} threshold control", - battery.name, - battery.pattern.description - ); - } - } - - Ok(supported_batteries) -} - -/// Applies the threshold settings to all supported batteries -fn apply_thresholds_to_batteries( - batteries: &[SupportedBattery<'_>], - start_threshold: u8, - stop_threshold: u8, -) -> Result<()> { - let mut errors = Vec::new(); - let mut success_count = 0; - - for battery in batteries { - let start_path = battery.path.join(battery.pattern.start_path); - let stop_path = battery.path.join(battery.pattern.stop_path); - - // Read current thresholds in case we need to restore them - let current_stop = sysfs::read_sysfs_value(&stop_path).ok(); - - // Write stop threshold first (must be >= start threshold) - let stop_result = sysfs::write_sysfs_value(&stop_path, &stop_threshold.to_string()); - - // Only proceed to set start threshold if stop threshold was set successfully - if matches!(stop_result, Ok(())) { - let start_result = sysfs::write_sysfs_value(&start_path, &start_threshold.to_string()); - - match start_result { - Ok(()) => { - log::debug!( - "Set {}-{}% charge thresholds for {} battery '{}'", - start_threshold, - stop_threshold, - battery.pattern.description, - battery.name - ); - success_count += 1; - } - Err(e) => { - // Start threshold failed, try to restore the previous stop threshold - if let Some(prev_stop) = ¤t_stop { - let restore_result = sysfs::write_sysfs_value(&stop_path, prev_stop); - if let Err(re) = restore_result { - log::warn!( - "Failed to restore previous stop threshold for battery '{}': {}. Battery may be in an inconsistent state.", - battery.name, - re - ); - } else { - log::debug!( - "Restored previous stop threshold ({}) for battery '{}'", - prev_stop, - battery.name - ); - } - } - - errors.push(format!( - "Failed to set start threshold for {} battery '{}': {}", - battery.pattern.description, battery.name, e - )); - } - } - } else if let Err(e) = stop_result { - errors.push(format!( - "Failed to set stop threshold for {} battery '{}': {}", - battery.pattern.description, battery.name, e - )); - } - } - - if success_count > 0 { - if !errors.is_empty() { - log::warn!( - "Partial success setting battery thresholds: {}", - errors.join("; ") - ); - } - Ok(()) - } else { - Err(ControlError::WriteError(format!( - "Failed to set charge thresholds on any battery: {}", - errors.join("; ") - ))) - } -} - -/// Determines if a power supply entry is a battery -fn is_battery(path: &Path) -> Result { - let type_path = path.join("type"); - - if !type_path.exists() { - return Ok(false); - } - - let ps_type = sysfs::read_sysfs_value(&type_path).map_err(|e| { - ControlError::ReadError(format!("Failed to read {}: {}", type_path.display(), e)) - })?; - - Ok(ps_type == "Battery") -} - -/// Identifies if a battery supports threshold control and which pattern it uses -fn find_battery_with_threshold_support(ps_path: &Path) -> Option> { - for pattern in THRESHOLD_PATTERNS { - let start_threshold_path = ps_path.join(pattern.start_path); - let stop_threshold_path = ps_path.join(pattern.stop_path); - - // Ensure both paths exist and are writable before considering this battery supported - if sysfs::path_exists_and_writable(&start_threshold_path) - && sysfs::path_exists_and_writable(&stop_threshold_path) - { - return Some(SupportedBattery { - name: ps_path.file_name()?.to_string_lossy().to_string(), - pattern, - path: ps_path.to_path_buf(), - }); - } - } - None -} diff --git a/src/config/types.rs b/src/config/types.rs index eb9ce7f..3150fc5 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -15,12 +15,12 @@ macro_rules! default_const { } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct BatteryChargeThresholds { +pub struct PowerSupplyChargeThresholds { pub start: u8, pub stop: u8, } -impl BatteryChargeThresholds { +impl PowerSupplyChargeThresholds { pub fn new(start: u8, stop: u8) -> Result { if stop == 0 { return Err(ConfigError::Validation( @@ -42,7 +42,7 @@ impl BatteryChargeThresholds { } } -impl TryFrom<(u8, u8)> for BatteryChargeThresholds { +impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { type Error = ConfigError; fn try_from(values: (u8, u8)) -> Result { @@ -66,7 +66,7 @@ pub struct ProfileConfig { #[serde(default)] pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, } impl Default for ProfileConfig { @@ -124,7 +124,7 @@ pub struct ProfileConfigToml { #[serde(default = "default_enable_auto_turbo")] pub enable_auto_turbo: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, } #[derive(Deserialize, Serialize, Debug, Clone, Default)] @@ -134,7 +134,7 @@ pub struct AppConfigToml { #[serde(default)] pub battery: ProfileConfigToml, #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, + pub battery_charge_thresholds: Option, pub ignored_power_supplies: Option>, #[serde(default)] pub daemon: DaemonConfigToml, diff --git a/src/engine.rs b/src/engine.rs index fadff3b..0aa2644 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,7 +1,7 @@ -use crate::battery; use crate::config::{AppConfig, ProfileConfig, TurboAutoSettings}; use crate::core::{OperationalMode, SystemReport, TurboSetting}; use crate::cpu::{self}; +use crate::power_supply; use crate::util::error::{ControlError, EngineError}; use std::sync::OnceLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -277,7 +277,7 @@ pub fn determine_and_apply_settings( if start_threshold < stop_threshold && stop_threshold <= 100 { log::info!("Setting battery charge thresholds: {start_threshold}-{stop_threshold}%"); - match battery::set_battery_charge_thresholds(start_threshold, stop_threshold) { + match power_supply::set_battery_charge_thresholds(start_threshold, stop_threshold) { Ok(()) => log::debug!("Battery charge thresholds set successfully"), Err(e) => log::warn!("Failed to set battery charge thresholds: {e}"), } diff --git a/src/main.rs b/src/main.rs index 333cf64..bc4b0c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod battery; mod cli; mod config; mod core; @@ -6,6 +5,7 @@ mod cpu; mod daemon; mod engine; mod monitor; +mod power_supply; mod util; use anyhow::{Context, anyhow, bail}; @@ -148,27 +148,14 @@ fn real_main() -> anyhow::Result<()> { cpu::set_platform_profile(platform_profile)?; } - // 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") - })?; - - 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})" - ); + for power_supply in power_supply::get_power_supplies()? { + if let Some(threshold_start) = charge_threshold_start { + power_supply::set_charge_threshold_start(&power_supply, threshold_start)?; } - battery::set_battery_charge_thresholds( - charge_threshold_start, - charge_threshold_end, - )?; + if let Some(threshold_end) = charge_threshold_end { + power_supply::set_charge_threshold_end(&power_supply, threshold_end)?; + } } Ok(()) diff --git a/src/power_supply.rs b/src/power_supply.rs new file mode 100644 index 0000000..7320f18 --- /dev/null +++ b/src/power_supply.rs @@ -0,0 +1,175 @@ +use anyhow::Context; + +use std::{ + fmt, fs, + 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 manufacturer: &'static str, + pub path_start: &'static str, + pub path_end: &'static str, +} + +/// Charge threshold configs. +const POWER_SUPPLY_CONFIGS: &[PowerSupplyConfig] = &[ + PowerSupplyConfig { + manufacturer: "Standard", + path_start: "charge_control_start_threshold", + path_end: "charge_control_end_threshold", + }, + PowerSupplyConfig { + manufacturer: "ASUS", + path_start: "charge_control_start_percentage", + path_end: "charge_control_end_percentage", + }, + // Combine Huawei and ThinkPad since they use identical paths. + PowerSupplyConfig { + manufacturer: "ThinkPad/Huawei", + path_start: "charge_start_threshold", + path_end: "charge_stop_threshold", + }, + // Framework laptop support. + PowerSupplyConfig { + manufacturer: "Framework", + path_start: "charge_behaviour_start_threshold", + path_end: "charge_behaviour_end_threshold", + }, +]; + +/// Represents a power supply that supports charge threshold control. +pub struct PowerSupply { + pub name: String, + pub path: PathBuf, + pub config: PowerSupplyConfig, +} + +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, + ) + } +} + +impl PowerSupply { + pub fn charge_threshold_path_start(&self) -> PathBuf { + self.path.join(self.config.path_start) + } + + pub fn charge_threshold_path_end(&self) -> PathBuf { + self.path.join(self.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(), + ) + }) +} + +fn is_power_supply(path: &Path) -> anyhow::Result { + let type_path = 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") +} + +/// Get all batteries in the system that support threshold control. +pub fn get_power_supplies() -> anyhow::Result> { + const PATH: &str = "/sys/class/power_supply"; + + let mut power_supplies = Vec::new(); + + 'entries: for entry in fs::read_dir(PATH).with_context(|| format!("failed to read '{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(); + + if !is_power_supply(&entry_path).with_context(|| { + format!( + "failed to determine whether if '{path}' is a power supply", + 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; + } + } + } + + 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(), + &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(), + &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(()) +} From baef8af9814114bff881fe2d1f994d3fd86b20ca Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:17:49 +0300 Subject: [PATCH 47/93] cli: remove governor_persist --- src/main.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index bc4b0c1..b86b414 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod monitor; mod power_supply; mod util; -use anyhow::{Context, anyhow, bail}; +use anyhow::Context; use clap::Parser as _; use std::fmt::Write as _; use std::io::Write as _; @@ -43,10 +43,6 @@ enum Command { #[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, @@ -103,7 +99,6 @@ fn real_main() -> anyhow::Result<()> { Command::Set { for_, governor, - governor_persist, energy_performance_preference, energy_performance_bias, frequency_mhz_minimum, From 63774803123fe29c89b0c20fa7a33f0b7d216d78 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Sun, 18 May 2025 23:38:44 +0300 Subject: [PATCH 48/93] 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 6ef4da91131c2259cd33108426e55aa2797e3588 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 16:17:54 +0300 Subject: [PATCH 49/93] 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 004e8e2a9c34f4db645cd2d1f7315463decb1717 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:40:45 +0300 Subject: [PATCH 50/93] 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 a14d88cee7e3ca70ebf13defc482dea29deba94c Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:43:21 +0300 Subject: [PATCH 51/93] 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 0ed0f18bb3f9b8aa22fb8b4095fdd0f74d20a921 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 17:59:44 +0300 Subject: [PATCH 52/93] 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 2704379b42469d9b5423680ffae2acdba5bab9aa Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 18:07:35 +0300 Subject: [PATCH 53/93] 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 2995909544627981a3f9a93d354faa7fd83c70ab Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 18:08:52 +0300 Subject: [PATCH 54/93] 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() From 0d3a88be036369ae2a1e6f77136d340def69b8f2 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:25:26 +0300 Subject: [PATCH 55/93] config: nuke old config and implement a new system --- Cargo.toml | 2 +- src/config.rs | 138 ++++++++++++++++++++++ src/config/load.rs | 128 -------------------- src/config/mod.rs | 5 - src/config/types.rs | 282 -------------------------------------------- src/cpu.rs | 4 +- src/main.rs | 73 +++--------- 7 files changed, 158 insertions(+), 474 deletions(-) create mode 100644 src/config.rs delete mode 100644 src/config/load.rs delete mode 100644 src/config/mod.rs delete mode 100644 src/config/types.rs diff --git a/Cargo.toml b/Cargo.toml index 3276b4a..287929e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ rust-version = "1.85" serde = { version = "1.0", features = ["derive"] } toml = "0.8" dirs = "6.0" -clap = { version = "4.0", features = ["derive"] } +clap = { version = "4.0", features = ["derive", "env"] } num_cpus = "1.16" ctrlc = "3.4" log = "0.4" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0e07031 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,138 @@ +use std::{fs, path::Path}; + +use anyhow::{Context, bail}; +use serde::{Deserialize, Serialize}; + +fn is_default(value: &T) -> bool { + *value == T::default() +} + +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] +pub struct CpuDelta { + /// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. + #[arg(short = 'c', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, + + /// Set the CPU governor. + #[arg(short = 'g', long)] + #[serde(skip_serializing_if = "is_default")] + pub governor: Option, // TODO: Validate with clap for available governors. + + /// Set CPU Energy Performance Preference (EPP). Short form: --epp. + #[arg(short = 'p', long, alias = "epp")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_preference: Option, // TODO: Validate with clap for available governors. + + /// Set CPU Energy Performance Bias (EPB). Short form: --epb. + #[arg(short = 'b', long, alias = "epb")] + #[serde(skip_serializing_if = "is_default")] + pub energy_performance_bias: Option, // TODO: Validate with clap for available governors. + + /// 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))] + #[serde(skip_serializing_if = "is_default")] + pub 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))] + #[serde(skip_serializing_if = "is_default")] + pub frequency_mhz_maximum: Option, + + /// Set turbo boost behaviour. Has to be for all CPUs. + #[arg(short = 't', long, conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub turbo: Option, +} + +#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] +pub struct PowerDelta { + /// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. + #[arg(short = 'p', long = "for")] + #[serde(rename = "for", skip_serializing_if = "is_default")] + pub for_: Option>, + + /// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. + #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_start: Option, + + /// Set the percentage where charging will stop. Short form: --charge-end. + #[arg(short = 'C', long, alias = "charge-end", value_parser = clap::value_parser!(u8).range(0..=100))] + #[serde(skip_serializing_if = "is_default")] + pub charge_threshold_end: Option, + + /// Set ACPI platform profile. Has to be for all power supplies. + #[arg(short = 'f', long, alias = "profile", conflicts_with = "for_")] + #[serde(skip_serializing_if = "is_default")] + pub platform_profile: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(untagged, rename_all = "kebab-case")] +pub enum Condition { + ChargeLessThan(u8), + ChargeMoreThan(u8), + + TemperatureLessThan(u8), + TemperatureMoreThan(u8), + + UtilizationLessThan(u8), + UtilizationMoreThan(u8), + + Charging, + OnBattery, + + False, + #[default] + True, + + All(Vec), + Any(Vec), + + Not(Box), +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct DaemonConfigLayer { + priority: u8, + + #[serde(default, skip_serializing_if = "is_default")] + if_: Condition, + + #[serde(default, skip_serializing_if = "is_default")] + cpu: CpuDelta, + #[serde(default, skip_serializing_if = "is_default")] + power: PowerDelta, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(transparent, default, rename_all = "kebab-case")] +pub struct DaemonConfig(pub Vec); + +impl DaemonConfig { + pub fn load_from(path: &Path) -> anyhow::Result { + let contents = fs::read_to_string(path).with_context(|| { + format!("failed to read config from '{path}'", path = path.display()) + })?; + + let config: Self = toml::from_str(&contents).context("failed to parse config file")?; + + { + let mut priorities = Vec::with_capacity(config.0.len()); + + for layer in &config.0 { + if priorities.contains(&layer.priority) { + bail!("each config layer must have a different priority") + } + + priorities.push(layer.priority); + } + } + + Ok(config) + } +} diff --git a/src/config/load.rs b/src/config/load.rs deleted file mode 100644 index 15f4248..0000000 --- a/src/config/load.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Configuration loading functionality -use std::fs; -use std::path::{Path, PathBuf}; - -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. -/// -/// # Arguments -/// -/// * `specific_path` - If provided, only attempts to load from this path and errors if not found -/// -/// # Returns -/// -/// * `Ok(AppConfig)` - Successfully loaded configuration -/// * `Err(ConfigError)` - Error loading or parsing configuration -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>) -> 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); - } - - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Specified config file not found: {}", path.display()), - ))?; - } - - // Check for SUPERFREQ_CONFIG environment variable - if let Ok(env_path) = std::env::var("SUPERFREQ_CONFIG") { - let env_path = Path::new(&env_path); - if env_path.exists() { - println!( - "Loading config from SUPERFREQ_CONFIG: {}", - env_path.display() - ); - return load_and_parse_config(env_path); - } - eprintln!( - "Warning: Config file specified by SUPERFREQ_CONFIG not found: {}", - env_path.display() - ); - } - - // System-wide paths - let config_paths = vec![ - PathBuf::from("/etc/xdg/superfreq/config.toml"), - PathBuf::from("/etc/superfreq.toml"), - ]; - - for path in config_paths { - if path.exists() { - println!("Loading config from: {}", path.display()); - match load_and_parse_config(&path) { - Ok(config) => return Ok(config), - Err(e) => { - eprintln!("Error with config file {}: {}", path.display(), e); - // Continue trying other files - } - } - } - } - - println!("No configuration file found or all failed to parse. Using default configuration."); - // Construct default AppConfig by converting default AppConfigToml - let default_toml_config = AppConfigToml::default(); - Ok(AppConfig { - charger: ProfileConfig::from(default_toml_config.charger), - battery: ProfileConfig::from(default_toml_config.battery), - ignored_power_supplies: default_toml_config.ignored_power_supplies, - daemon: DaemonConfig::default(), - }) -} - -/// Load and parse a configuration file -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).context("failed to parse config toml")?; - - // Handle inheritance of values from global to profile configs - let mut charger_profile = toml_app_config.charger.clone(); - let mut battery_profile = toml_app_config.battery.clone(); - - // Clone global battery_charge_thresholds once if it exists - if let Some(global_thresholds) = toml_app_config.battery_charge_thresholds { - // Apply to charger profile if not already set - if charger_profile.battery_charge_thresholds.is_none() { - charger_profile.battery_charge_thresholds = Some(global_thresholds.clone()); - } - - // Apply to battery profile if not already set - if battery_profile.battery_charge_thresholds.is_none() { - battery_profile.battery_charge_thresholds = Some(global_thresholds); - } - } - - // Convert AppConfigToml to AppConfig - Ok(AppConfig { - charger: ProfileConfig::from(charger_profile), - battery: ProfileConfig::from(battery_profile), - ignored_power_supplies: toml_app_config.ignored_power_supplies, - daemon: DaemonConfig { - poll_interval_sec: toml_app_config.daemon.poll_interval_sec, - adaptive_interval: toml_app_config.daemon.adaptive_interval, - min_poll_interval_sec: toml_app_config.daemon.min_poll_interval_sec, - max_poll_interval_sec: toml_app_config.daemon.max_poll_interval_sec, - throttle_on_battery: toml_app_config.daemon.throttle_on_battery, - log_level: toml_app_config.daemon.log_level, - stats_file_path: toml_app_config.daemon.stats_file_path, - }, - }) -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index c2f3076..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod load; -pub mod types; - -pub use load::*; -pub use types::*; diff --git a/src/config/types.rs b/src/config/types.rs deleted file mode 100644 index c0be6e2..0000000 --- a/src/config/types.rs +++ /dev/null @@ -1,282 +0,0 @@ -use anyhow::bail; -// Configuration types and structures for superfreq -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. -macro_rules! default_const { - ($($name:ident -> $type:ty = $value:expr;)*) => { - $( - const fn $name() -> $type { - $value - } - )* - }; -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct PowerSupplyChargeThresholds { - pub start: u8, - pub stop: u8, -} - -impl TryFrom<(u8, u8)> for PowerSupplyChargeThresholds { - type Error = anyhow::Error; - - fn try_from((start, stop): (u8, u8)) -> anyhow::Result { - if stop == 0 { - bail!("stop threshold must be greater than 0%"); - } - if start >= stop { - bail!("start threshold ({start}) must be less than stop threshold ({stop})"); - } - if stop > 100 { - bail!("stop threshold ({stop}) cannot exceed 100%"); - } - - Ok(PowerSupplyChargeThresholds { start, stop }) - } -} - -// Structs for configuration using serde::Deserialize -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ProfileConfig { - pub governor: 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, - pub max_freq_mhz: Option, - pub platform_profile: Option, - #[serde(default)] - pub turbo_auto_settings: TurboAutoSettings, - #[serde(default)] - pub enable_auto_turbo: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, -} - -impl Default for ProfileConfig { - fn default() -> Self { - Self { - governor: Some("schedutil".to_string()), // common sensible default (?) - turbo: None, - epp: None, // defaults depend on governor and system - epb: None, // defaults depend on governor and system - min_freq_mhz: None, // no override - max_freq_mhz: None, // no override - platform_profile: None, // no override - turbo_auto_settings: TurboAutoSettings::default(), - enable_auto_turbo: default_enable_auto_turbo(), - battery_charge_thresholds: None, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -pub struct AppConfig { - #[serde(default)] - pub charger: ProfileConfig, - #[serde(default)] - pub battery: ProfileConfig, - pub ignored_power_supplies: Option>, - #[serde(default)] - pub daemon: DaemonConfig, -} - -// Intermediate structs for TOML parsing -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ProfileConfigToml { - pub governor: Option, - pub turbo: Option, // "always", "auto", "never" - pub epp: Option, - pub epb: Option, - pub min_freq_mhz: Option, - pub max_freq_mhz: Option, - pub platform_profile: Option, - pub turbo_auto_settings: Option, - #[serde(default = "default_enable_auto_turbo")] - pub enable_auto_turbo: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone, Default)] -pub struct AppConfigToml { - #[serde(default)] - pub charger: ProfileConfigToml, - #[serde(default)] - pub battery: ProfileConfigToml, - #[serde(skip_serializing_if = "Option::is_none")] - pub battery_charge_thresholds: Option, - pub ignored_power_supplies: Option>, - #[serde(default)] - pub daemon: DaemonConfigToml, -} - -impl Default for ProfileConfigToml { - fn default() -> Self { - Self { - governor: Some("schedutil".to_string()), - turbo: Some("auto".to_string()), - epp: None, - epb: None, - min_freq_mhz: None, - max_freq_mhz: None, - platform_profile: None, - turbo_auto_settings: None, - enable_auto_turbo: default_enable_auto_turbo(), - battery_charge_thresholds: None, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct TurboAutoSettings { - #[serde(default = "default_load_threshold_high")] - pub load_threshold_high: f32, - #[serde(default = "default_load_threshold_low")] - pub load_threshold_low: f32, - #[serde(default = "default_temp_threshold_high")] - pub temp_threshold_high: f32, - /// Initial turbo boost state when no previous state exists. - /// Set to `true` to start with turbo enabled, `false` to start with turbo disabled. - /// This is only used at first launch or after a reset. - #[serde(default = "default_initial_turbo_state")] - pub initial_turbo_state: bool, -} - -// Default thresholds for Auto turbo mode -pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is above this -pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this -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_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 { - Self { - load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, - load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, - temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, - initial_turbo_state: DEFAULT_INITIAL_TURBO_STATE, - } - } -} - -impl From for ProfileConfig { - fn from(toml_config: ProfileConfigToml) -> Self { - Self { - governor: toml_config.governor, - turbo: toml_config - .turbo - .and_then(|s| match s.to_lowercase().as_str() { - "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, - min_freq_mhz: toml_config.min_freq_mhz, - max_freq_mhz: toml_config.max_freq_mhz, - platform_profile: toml_config.platform_profile, - turbo_auto_settings: toml_config.turbo_auto_settings.unwrap_or_default(), - enable_auto_turbo: toml_config.enable_auto_turbo, - battery_charge_thresholds: toml_config.battery_charge_thresholds, - } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct DaemonConfig { - #[serde(default = "default_poll_interval_sec")] - pub poll_interval_sec: u64, - #[serde(default = "default_adaptive_interval")] - pub adaptive_interval: bool, - #[serde(default = "default_min_poll_interval_sec")] - pub min_poll_interval_sec: u64, - #[serde(default = "default_max_poll_interval_sec")] - pub max_poll_interval_sec: u64, - #[serde(default = "default_throttle_on_battery")] - pub throttle_on_battery: bool, - #[serde(default = "default_log_level")] - pub log_level: LogLevel, - #[serde(default = "default_stats_file_path")] - pub stats_file_path: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogLevel { - Error, - Warning, - Info, - Debug, -} - -impl Default for DaemonConfig { - fn default() -> Self { - Self { - poll_interval_sec: default_poll_interval_sec(), - adaptive_interval: default_adaptive_interval(), - min_poll_interval_sec: default_min_poll_interval_sec(), - max_poll_interval_sec: default_max_poll_interval_sec(), - throttle_on_battery: default_throttle_on_battery(), - log_level: default_log_level(), - stats_file_path: default_stats_file_path(), - } - } -} - -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 { - #[serde(default = "default_poll_interval_sec")] - pub poll_interval_sec: u64, - #[serde(default = "default_adaptive_interval")] - pub adaptive_interval: bool, - #[serde(default = "default_min_poll_interval_sec")] - pub min_poll_interval_sec: u64, - #[serde(default = "default_max_poll_interval_sec")] - pub max_poll_interval_sec: u64, - #[serde(default = "default_throttle_on_battery")] - pub throttle_on_battery: bool, - #[serde(default = "default_log_level")] - pub log_level: LogLevel, - #[serde(default = "default_stats_file_path")] - pub stats_file_path: Option, -} - -impl Default for DaemonConfigToml { - fn default() -> Self { - Self { - poll_interval_sec: default_poll_interval_sec(), - adaptive_interval: default_adaptive_interval(), - min_poll_interval_sec: default_min_poll_interval_sec(), - max_poll_interval_sec: default_max_poll_interval_sec(), - throttle_on_battery: default_throttle_on_battery(), - log_level: default_log_level(), - stats_file_path: default_stats_file_path(), - } - } -} diff --git a/src/cpu.rs b/src/cpu.rs index 2d7a32d..d0985e9 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -14,7 +14,7 @@ fn read_u64(path: impl AsRef) -> anyhow::Result { let content = fs::read_to_string(path)?; - Ok(content.trim().parse::()?) + Ok(content.trim().parse()?) } fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { @@ -73,7 +73,7 @@ impl Cpu { }; // Has to match "cpu{N}". - let Ok(number) = cpu_prefix_removed.parse::() else { + let Ok(number) = cpu_prefix_removed.parse() else { continue; }; diff --git a/src/main.rs b/src/main.rs index 9902b79..68b929f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use anyhow::Context; use clap::Parser as _; use std::fmt::Write as _; use std::io::Write as _; +use std::path::PathBuf; use std::{io, process}; use yansi::Paint as _; @@ -29,57 +30,17 @@ enum Command { Info, /// Start the daemon. - Start, + Start { + /// The daemon config path. + #[arg(long, env = "SUPERFREQ_CONFIG")] + config: PathBuf, + }, /// 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(short = 'g', long)] - governor: Option, // TODO: Validate with clap for available governors. - - /// Set CPU Energy Performance Preference (EPP). Short form: --epp. - #[arg(short = 'p', long, alias = "epp")] - energy_performance_preference: Option, - - /// Set CPU Energy Performance Bias (EPB). Short form: --epb. - #[arg(short = 'b', 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(short = 't', long, conflicts_with = "for_")] - turbo: Option, - }, + CpuSet(config::CpuDelta), /// 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 = '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 = '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, - }, + PowerSet(config::PowerDelta), } fn real_main() -> anyhow::Result<()> { @@ -91,17 +52,17 @@ fn real_main() -> anyhow::Result<()> { .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(()) + Command::Start { config } => { + let config = config::DaemonConfig::load_from(&config) + .context("failed to load daemon config file")?; + + daemon::run(config) } - Command::CpuSet { + Command::CpuSet(config::CpuDelta { for_, governor, energy_performance_preference, @@ -109,7 +70,7 @@ fn real_main() -> anyhow::Result<()> { frequency_mhz_minimum, frequency_mhz_maximum, turbo, - } => { + }) => { let cpus = match for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); @@ -152,12 +113,12 @@ fn real_main() -> anyhow::Result<()> { Ok(()) } - Command::PowerSet { + Command::PowerSet(config::PowerDelta { for_, charge_threshold_start, charge_threshold_end, platform_profile, - } => { + }) => { let power_supplies = match for_ { Some(names) => { let mut power_supplies = Vec::with_capacity(names.len()); From c073b640dcb3e0197c012e8114dd31ea2c41fc23 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:32:08 +0300 Subject: [PATCH 56/93] config: fix schema, toml does not have top level lists --- src/config.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0e07031..47de929 100644 --- a/src/config.rs +++ b/src/config.rs @@ -111,7 +111,9 @@ pub struct DaemonConfigLayer { #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[serde(transparent, default, rename_all = "kebab-case")] -pub struct DaemonConfig(pub Vec); +pub struct DaemonConfig { + config: Vec, +} impl DaemonConfig { pub fn load_from(path: &Path) -> anyhow::Result { @@ -122,9 +124,9 @@ impl DaemonConfig { let config: Self = toml::from_str(&contents).context("failed to parse config file")?; { - let mut priorities = Vec::with_capacity(config.0.len()); + let mut priorities = Vec::with_capacity(config.config.len()); - for layer in &config.0 { + for layer in &config.config { if priorities.contains(&layer.priority) { bail!("each config layer must have a different priority") } From ca4b1dbc9279239fa66e5dd519c40fe9124a9ba5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:36:36 +0300 Subject: [PATCH 57/93] main: move application to deltas, comment out broken modules for now --- src/config.rs | 84 ++++++++++++++++++++++++++++++++++++++++ src/cpu.rs | 8 ++-- src/main.rs | 103 ++++---------------------------------------------- 3 files changed, 96 insertions(+), 99 deletions(-) diff --git a/src/config.rs b/src/config.rs index 47de929..cc9f18c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,8 @@ use std::{fs, path::Path}; use anyhow::{Context, bail}; use serde::{Deserialize, Serialize}; +use crate::{cpu, power_supply}; + fn is_default(value: &T) -> bool { *value == T::default() } @@ -46,6 +48,51 @@ pub struct CpuDelta { pub turbo: Option, } +impl CpuDelta { + pub fn apply(&self) -> anyhow::Result<()> { + let cpus = match &self.for_ { + 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) = self.governor.as_ref() { + cpu.set_governor(governor)?; + } + + if let Some(epp) = self.energy_performance_preference.as_ref() { + cpu.set_epp(epp)?; + } + + if let Some(epb) = self.energy_performance_bias.as_ref() { + cpu.set_epb(epb)?; + } + + if let Some(mhz_minimum) = self.frequency_mhz_minimum { + cpu.set_frequency_minimum(mhz_minimum)?; + } + + if let Some(mhz_maximum) = self.frequency_mhz_maximum { + cpu.set_frequency_maximum(mhz_maximum)?; + } + } + + if let Some(turbo) = self.turbo { + cpu::Cpu::set_turbo(turbo)?; + } + + Ok(()) + } +} + #[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")] pub struct PowerDelta { @@ -70,6 +117,43 @@ pub struct PowerDelta { pub platform_profile: Option, } +impl PowerDelta { + pub fn apply(&self) -> anyhow::Result<()> { + let power_supplies = match &self.for_ { + Some(names) => { + let mut power_supplies = Vec::with_capacity(names.len()); + + for name in names { + power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?); + } + + power_supplies + } + + None => power_supply::PowerSupply::all()? + .into_iter() + .filter(|power_supply| power_supply.threshold_config.is_some()) + .collect(), + }; + + for power_supply in power_supplies { + if let Some(threshold_start) = self.charge_threshold_start { + power_supply.set_charge_threshold_start(threshold_start)?; + } + + if let Some(threshold_end) = self.charge_threshold_end { + power_supply.set_charge_threshold_end(threshold_end)?; + } + } + + if let Some(platform_profile) = self.platform_profile.as_ref() { + power_supply::PowerSupply::set_platform_profile(platform_profile)?; + } + + Ok(()) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] #[serde(untagged, rename_all = "kebab-case")] pub enum Condition { diff --git a/src/cpu.rs b/src/cpu.rs index d0985e9..4d9d551 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -236,7 +236,7 @@ impl Cpu { 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_mhz * 1000; let frequency_khz = frequency_khz.to_string(); write( @@ -258,7 +258,7 @@ impl Cpu { return Ok(()); }; - if new_frequency_mhz as u64 * 1000 < minimum_frequency_khz { + if new_frequency_mhz * 1000 < minimum_frequency_khz { bail!( "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}", minimum_frequency_khz / 1000, @@ -274,7 +274,7 @@ impl Cpu { 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_mhz * 1000; let frequency_khz = frequency_khz.to_string(); write( @@ -344,7 +344,7 @@ impl Cpu { let Cpu { number, .. } = cpu; write( - &format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), + format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), value_boost, ) .is_ok() diff --git a/src/main.rs b/src/main.rs index 68b929f..52f33c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ mod config; -mod core; +// mod core; mod cpu; -mod daemon; -mod engine; -mod monitor; +// mod daemon; +// mod engine; +// mod monitor; mod power_supply; use anyhow::Context; @@ -56,102 +56,15 @@ fn real_main() -> anyhow::Result<()> { Command::Info => todo!(), Command::Start { config } => { - let config = config::DaemonConfig::load_from(&config) + let _config = config::DaemonConfig::load_from(&config) .context("failed to load daemon config file")?; - daemon::run(config) - } - - Command::CpuSet(config::CpuDelta { - for_, - governor, - energy_performance_preference, - energy_performance_bias, - frequency_mhz_minimum, - frequency_mhz_maximum, - turbo, - }) => { - let cpus = match for_ { - 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)?; - } - - if let Some(epp) = energy_performance_preference.as_ref() { - cpu.set_epp(epp)?; - } - - if let Some(epb) = energy_performance_bias.as_ref() { - cpu.set_epb(epb)?; - } - - if let Some(mhz_minimum) = frequency_mhz_minimum { - cpu.set_frequency_minimum(mhz_minimum)?; - } - - if let Some(mhz_maximum) = frequency_mhz_maximum { - cpu.set_frequency_maximum(mhz_maximum)?; - } - } - - if let Some(turbo) = turbo { - cpu::Cpu::set_turbo(turbo)?; - } - + // daemon::run(config) Ok(()) } - Command::PowerSet(config::PowerDelta { - for_, - charge_threshold_start, - charge_threshold_end, - platform_profile, - }) => { - let power_supplies = match for_ { - Some(names) => { - let mut power_supplies = Vec::with_capacity(names.len()); - - for name in names { - power_supplies.push(power_supply::PowerSupply::from_name(name)?); - } - - power_supplies - } - - None => power_supply::PowerSupply::all()? - .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(threshold_start)?; - } - - if let Some(threshold_end) = charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end)?; - } - } - - if let Some(platform_profile) = platform_profile.as_ref() { - power_supply::PowerSupply::set_platform_profile(platform_profile); - } - - Ok(()) - } + Command::CpuSet(delta) => delta.apply(), + Command::PowerSet(delta) => delta.apply(), } } From b6d4e09c7f3408e5fa6a89942560bcb0d9783d38 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:44:27 +0300 Subject: [PATCH 58/93] power_supply&cpu: somewhat improve error messages --- src/config.rs | 2 +- src/cpu.rs | 11 +++++++---- src/power_supply.rs | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index cc9f18c..5d528b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -60,7 +60,7 @@ impl CpuDelta { cpus } - None => cpu::Cpu::all()?, + None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, }; for cpu in cpus { diff --git a/src/cpu.rs b/src/cpu.rs index 4d9d551..ceac01d 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -92,10 +92,13 @@ impl Cpu { /// 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, - )); + let Self { number, .. } = self; + + if !exists(format!("/sys/devices/system/cpu/cpu{number}")) { + bail!("{self} does not exist"); + } + + let has_cpufreq = exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq = has_cpufreq; diff --git a/src/power_supply.rs b/src/power_supply.rs index 9ec00ab..12649b8 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -62,12 +62,7 @@ pub struct PowerSupply { impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "power supply '{name}' at '{path}'", - name = &self.name, - path = self.path.display(), - )?; + write!(f, "power supply '{name}'", name = &self.name)?; if let Some(config) = self.threshold_config.as_ref() { write!( @@ -147,6 +142,10 @@ impl PowerSupply { } pub fn rescan(&mut self) -> anyhow::Result<()> { + if !self.path.exists() { + bail!("{self} does not exist"); + } + let threshold_config = self .get_type() .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? From f3813230c58717523d3fe458ac3fa6715d0537f7 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 21:47:56 +0300 Subject: [PATCH 59/93] power_supply&cpu: kolor --- src/cpu.rs | 4 +++- src/power_supply.rs | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index ceac01d..17a5da1 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,4 +1,5 @@ use anyhow::{Context, bail}; +use yansi::Paint as _; use std::{fmt, fs, path::Path, string::ToString}; @@ -36,7 +37,8 @@ pub struct Cpu { impl fmt::Display for Cpu { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { number, .. } = self; + let number = self.number.cyan(); + write!(f, "CPU {number}") } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 12649b8..1f69a3c 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,4 +1,5 @@ use anyhow::{Context, bail}; +use yansi::Paint as _; use std::{ fmt, fs, @@ -62,13 +63,13 @@ 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}'", name = self.name.yellow())?; if let Some(config) = self.threshold_config.as_ref() { write!( f, " from manufacturer '{manufacturer}'", - manufacturer = config.manufacturer, + manufacturer = config.manufacturer.green(), )?; } From 0de81054328c93e0acd97ffcc69d4e1323026d7b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 22:22:26 +0300 Subject: [PATCH 60/93] config: better more enhanched expression --- src/config.rs | 108 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5d528b8..f8fb584 100644 --- a/src/config.rs +++ b/src/config.rs @@ -154,38 +154,103 @@ impl PowerDelta { } } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(untagged, rename_all = "kebab-case")] -pub enum Condition { - ChargeLessThan(u8), - ChargeMoreThan(u8), +pub enum Expression { + #[serde(rename = "$cpu-temperature")] + CpuTemperature, - TemperatureLessThan(u8), - TemperatureMoreThan(u8), + #[serde(rename = "%cpu-volatility")] + CpuVolatility, - UtilizationLessThan(u8), - UtilizationMoreThan(u8), + #[serde(rename = "%cpu-utilization")] + CpuUtilization, + #[serde(rename = "%power-supply-charge")] + PowerSupplyCharge, + + #[serde(rename = "%power-supply-discharge-rate")] + PowerSupplyDischargeRate, + + #[serde(rename = "?charging")] Charging, + #[serde(rename = "?on-battery")] OnBattery, + #[serde(rename = "#false")] False, + #[default] + #[serde(rename = "#true")] True, - All(Vec), - Any(Vec), + Number(f64), - Not(Box), + Plus { + value: Box, + plus: Box, + }, + Minus { + value: Box, + minus: Box, + }, + Multiply { + value: Box, + multiply: Box, + }, + Power { + value: Box, + power: Box, + }, + Divide { + value: Box, + divide: Box, + }, + + LessThan { + value: Box, + is_less_than: Box, + }, + + MoreThan { + value: Box, + is_more_than: Box, + }, + + Equal { + value: Box, + is_equal: Box, + leeway: Box, + }, + + And { + value: Box, + and: Box, + }, + All { + all: Vec, + }, + + Or { + value: Box, + or: Box, + }, + Any { + any: Vec, + }, + + Not { + not: Box, + }, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] -pub struct DaemonConfigLayer { +pub struct Rule { priority: u8, #[serde(default, skip_serializing_if = "is_default")] - if_: Condition, + if_: Expression, #[serde(default, skip_serializing_if = "is_default")] cpu: CpuDelta, @@ -193,10 +258,11 @@ pub struct DaemonConfigLayer { power: PowerDelta, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(transparent, default, rename_all = "kebab-case")] pub struct DaemonConfig { - config: Vec, + #[serde(rename = "rule")] + rules: Vec, } impl DaemonConfig { @@ -208,14 +274,14 @@ impl DaemonConfig { let config: Self = toml::from_str(&contents).context("failed to parse config file")?; { - let mut priorities = Vec::with_capacity(config.config.len()); + let mut priorities = Vec::with_capacity(config.rules.len()); - for layer in &config.config { - if priorities.contains(&layer.priority) { - bail!("each config layer must have a different priority") + for rule in &config.rules { + if priorities.contains(&rule.priority) { + bail!("each config rule must have a different priority") } - priorities.push(layer.priority); + priorities.push(rule.priority); } } From 606cedb68a2a9fe37e3710e5ec71f6bbc0ea1ccc Mon Sep 17 00:00:00 2001 From: RGBCube Date: Mon, 19 May 2025 22:32:54 +0300 Subject: [PATCH 61/93] daemon: wip new impl --- src/config.rs | 14 +- src/daemon.rs | 771 +++++++++++----------------------------------- src/daemon_old.rs | 649 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 7 +- 4 files changed, 834 insertions(+), 607 deletions(-) create mode 100644 src/daemon_old.rs diff --git a/src/config.rs b/src/config.rs index f8fb584..585b7a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -157,14 +157,20 @@ impl PowerDelta { #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(untagged, rename_all = "kebab-case")] pub enum Expression { + #[serde(rename = "%cpu-usage")] + CpuUsage, + + #[serde(rename = "$cpu-usage-volatility")] + CpuUsageVolatility, + #[serde(rename = "$cpu-temperature")] CpuTemperature, - #[serde(rename = "%cpu-volatility")] - CpuVolatility, + #[serde(rename = "$cpu-temperature-volatility")] + CpuTemperatureVolatility, - #[serde(rename = "%cpu-utilization")] - CpuUtilization, + #[serde(rename = "$cpu-idle-seconds")] + CpuIdleSeconds, #[serde(rename = "%power-supply-charge")] PowerSupplyCharge, diff --git a/src/daemon.rs b/src/daemon.rs index ba6d37d..55241a5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,649 +1,222 @@ -use anyhow::Context; -use anyhow::bail; +use std::{ + collections::VecDeque, + ops, + time::{Duration, Instant}, +}; -use crate::config::AppConfig; -use crate::core::SystemReport; -use crate::engine; -use crate::monitor; -use std::collections::VecDeque; -use std::fs::File; -use std::io::Write; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; +use crate::config; -/// Parameters for computing optimal polling interval -struct IntervalParams { - /// Base polling interval in seconds - base_interval: u64, - /// Minimum allowed polling interval in seconds - min_interval: u64, - /// Maximum allowed polling interval in seconds - max_interval: u64, - /// How rapidly CPU usage is changing - cpu_volatility: f32, - /// How rapidly temperature is changing - temp_volatility: f32, - /// Battery discharge rate in %/hour if available - battery_discharge_rate: Option, - /// Time since last detected user activity - last_user_activity: Duration, - /// Whether the system appears to be idle - is_system_idle: bool, - /// Whether the system is running on battery power - on_battery: bool, -} - -/// Calculate the idle time multiplier based on system idle duration +/// Calculate the idle time multiplier based on system idle time. /// -/// Returns a multiplier between 1.0 and 5.0 (capped): +/// Returns a multiplier between 1.0 and 5.0: /// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 /// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) -fn idle_multiplier(idle_secs: u64) -> f32 { - if idle_secs == 0 { - return 1.0; // No idle time, no multiplier effect - } - - let idle_factor = if idle_secs < 120 { - // Less than 2 minutes (0 to 119 seconds) +fn idle_multiplier(idle_for: Duration) -> f64 { + let factor = match idle_for.as_secs() < 120 { + // Less than 2 minutes. // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) - 1.0 + (idle_secs as f32) / 120.0 - } else { - // 2 minutes (120 seconds) or more - let idle_time_minutes = idle_secs / 60; + true => (idle_for.as_secs() as f64) / 120.0, + + // 2 minutes or more. // Logarithmic scaling: 1.0 + log2(minutes) - 1.0 + (idle_time_minutes as f32).log2().max(0.5) + false => { + let idle_minutes = idle_for.as_secs() as f64 / 60.0; + idle_minutes.log2() + } }; - // Cap the multiplier to avoid excessive intervals - idle_factor.min(5.0) // max factor of 5x + // Clamp the multiplier to avoid excessive intervals. + (1.0 + factor).clamp(1.0, 5.0) } -/// 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) -> anyhow::Result { - // Use the centralized validation function - validate_poll_intervals(params.min_interval, params.max_interval)?; - - // Start with base interval - let mut adjusted_interval = params.base_interval; - - // If we're on battery, we want to be more aggressive about saving power - if params.on_battery { - // Apply a multiplier based on battery discharge rate - if let Some(discharge_rate) = params.battery_discharge_rate { - if discharge_rate > 20.0 { - // High discharge rate - increase polling interval significantly (3x) - adjusted_interval = adjusted_interval.saturating_mul(3); - } else if discharge_rate > 10.0 { - // Moderate discharge - double polling interval (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } else { - // Low discharge rate - increase by 50% (multiply by 3/2) - adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); - } - } else { - // If we don't know discharge rate, use a conservative multiplier (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } - } - - // Adjust for system idleness - if params.is_system_idle { - let idle_time_seconds = params.last_user_activity.as_secs(); - - // Apply adjustment only if the system has been idle for a non-zero duration - if idle_time_seconds > 0 { - let idle_factor = idle_multiplier(idle_time_seconds); - - log::debug!( - "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", - idle_time_seconds, - (idle_time_seconds as f32 / 60.0).round(), - idle_factor - ); - - // Convert f32 multiplier to integer-safe math - // Multiply by a large number first, then divide to maintain precision - // Use 1000 as the scaling factor to preserve up to 3 decimal places - let scaling_factor = 1000; - let scaled_factor = (idle_factor * scaling_factor as f32) as u64; - adjusted_interval = adjusted_interval - .saturating_mul(scaled_factor) - .saturating_div(scaling_factor); - } - // If idle_time_seconds is 0, no factor is applied by this block - } - - // Adjust for CPU/temperature volatility - if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - // For division by 2 (halving the interval), we can safely use integer division - adjusted_interval = (adjusted_interval / 2).max(1); - } - - // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval - let min_safe_interval = params.min_interval.max(1); - let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); - - // Blend the new interval with the cached value if available - let blended_interval = if let Some(cached) = system_history.last_computed_interval { - // Use a weighted average: 70% previous value, 30% new value - // This smooths out drastic changes in polling frequency - const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% - const NEW_VALUE_WEIGHT: u128 = 3; // 30% - const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 - - // XXX: Use u128 arithmetic to avoid overflow with large interval values - let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT - + u128::from(new_interval) * NEW_VALUE_WEIGHT) - / TOTAL_WEIGHT; - - result as u64 - } else { - new_interval - }; - - // Blended result still needs to respect the configured bounds - // Again enforce minimum of 1 second regardless of params.min_interval - Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) -} - -/// Tracks historical system data for "advanced" adaptive polling -#[derive(Debug)] -struct SystemHistory { - /// Last several CPU usage measurements - cpu_usage_history: VecDeque, - /// Last several temperature readings - temperature_history: VecDeque, - /// Time of last detected user activity +struct Daemon { + /// Last time when there was user activity. last_user_activity: Instant, - /// Previous battery percentage (to calculate discharge rate) - last_battery_percentage: Option, - /// Timestamp of last battery reading - last_battery_timestamp: Option, - /// Battery discharge rate (%/hour) - battery_discharge_rate: Option, - /// Time spent in each system state - state_durations: std::collections::HashMap, - /// Last time a state transition happened - last_state_change: Instant, - /// Current system state - current_state: SystemState, - /// Last computed optimal polling interval - last_computed_interval: Option, + + /// CPU usage and temperature log. + cpu_log: VecDeque, + + /// Power supply status log. + power_supply_log: VecDeque, + + charging: bool, } -impl Default for SystemHistory { - fn default() -> Self { - Self { - cpu_usage_history: VecDeque::new(), - temperature_history: VecDeque::new(), - last_user_activity: Instant::now(), - last_battery_percentage: None, - last_battery_timestamp: None, - battery_discharge_rate: None, - state_durations: std::collections::HashMap::new(), - last_state_change: Instant::now(), - current_state: SystemState::default(), - last_computed_interval: None, - } - } +struct CpuLog { + at: Instant, + + /// CPU usage between 0-1, a percentage. + usage: f64, + + /// CPU temperature in celcius. + temperature: f64, } -impl SystemHistory { - /// Update system history with new report data - fn update(&mut self, report: &SystemReport) { - // Update CPU usage history - if !report.cpu_cores.is_empty() { - let mut total_usage: f32 = 0.0; - let mut core_count: usize = 0; +struct CpuVolatility { + at: ops::Range, - for core in &report.cpu_cores { - if let Some(usage) = core.usage_percent { - total_usage += usage; - core_count += 1; - } - } + usage: f64, - if core_count > 0 { - let avg_usage = total_usage / core_count as f32; + temperature: f64, +} - // Keep only the last 5 measurements - if self.cpu_usage_history.len() >= 5 { - self.cpu_usage_history.pop_front(); - } - self.cpu_usage_history.push_back(avg_usage); - - // Update last_user_activity if CPU usage indicates activity - // Consider significant CPU usage or sudden change as user activity - if avg_usage > 20.0 - || (self.cpu_usage_history.len() > 1 - && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) - .abs() - > 15.0) - { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on CPU usage"); - } - } +impl Daemon { + fn cpu_volatility(&self) -> Option { + if self.cpu_log.len() < 2 { + return None; } - // Update temperature history - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if self.temperature_history.len() >= 5 { - self.temperature_history.pop_front(); - } - self.temperature_history.push_back(temp); + let change_count = self.cpu_log.len() - 1; - // Significant temperature increase can indicate user activity - if self.temperature_history.len() > 1 { - let temp_change = - temp - self.temperature_history[self.temperature_history.len() - 2]; - if temp_change > 5.0 { - // 5°C rise in temperature - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on temperature change"); - } - } + let mut usage_change_sum = 0.0; + let mut temperature_change_sum = 0.0; + + for index in 0..change_count { + let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage; + usage_change_sum += usage_change.abs(); + + let temperature_change = + self.cpu_log[index + 1].temperature - self.cpu_log[index].temperature; + temperature_change_sum += temperature_change.abs(); } - // Update battery discharge rate - if let Some(battery) = report.batteries.first() { - // Reset when we are charging or have just connected AC - if battery.ac_connected { - // Reset discharge tracking but continue updating the rest of - // the history so we still detect activity/load changes on AC. - self.battery_discharge_rate = None; - self.last_battery_percentage = None; - self.last_battery_timestamp = None; - } + Some(CpuVolatility { + at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at, - if let Some(current_percentage) = battery.capacity_percent { - let current_percent = f32::from(current_percentage); - - if let (Some(last_percentage), Some(last_timestamp)) = - (self.last_battery_percentage, self.last_battery_timestamp) - { - let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; - // Only calculate discharge rate if at least 30 seconds have passed - // and we're not on AC power - if elapsed_hours > 0.0083 && !battery.ac_connected { - // 0.0083 hours = 30 seconds - // Calculate discharge rate in percent per hour - let percent_change = last_percentage - current_percent; - if percent_change > 0.0 { - // Only if battery is discharging - let hourly_rate = percent_change / elapsed_hours; - // Clamp the discharge rate to a reasonable maximum value (100%/hour) - let clamped_rate = hourly_rate.min(100.0); - self.battery_discharge_rate = Some(clamped_rate); - } - } - } - - self.last_battery_percentage = Some(current_percent); - self.last_battery_timestamp = Some(Instant::now()); - } - } - - // Update system state tracking - let new_state = determine_system_state(report, self); - if new_state != self.current_state { - // Record time spent in previous state - let time_in_state = self.last_state_change.elapsed(); - *self - .state_durations - .entry(self.current_state.clone()) - .or_insert(Duration::ZERO) += time_in_state; - - // State changes (except to Idle) likely indicate user activity - if new_state != SystemState::Idle && new_state != SystemState::LowLoad { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system state change to {new_state:?}"); - } - - // Update state - self.current_state = new_state; - self.last_state_change = Instant::now(); - } - - // Check for significant load changes - if report.system_load.load_avg_1min > 1.0 { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system load"); - } + usage: usage_change_sum / change_count as f64, + temperature: temperature_change_sum / change_count as f64, + }) } - /// Calculate CPU usage volatility (how much it's changing) - fn get_cpu_volatility(&self) -> f32 { - if self.cpu_usage_history.len() < 2 { - return 0.0; - } + fn is_cpu_idle(&self) -> bool { + let recent_log_count = self + .cpu_log + .iter() + .rev() + .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) + .count(); - let mut sum_of_changes = 0.0; - for i in 1..self.cpu_usage_history.len() { - sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); - } - - sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 - } - - /// Calculate temperature volatility - fn get_temperature_volatility(&self) -> f32 { - if self.temperature_history.len() < 2 { - return 0.0; - } - - let mut sum_of_changes = 0.0; - for i in 1..self.temperature_history.len() { - sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); - } - - sum_of_changes / (self.temperature_history.len() - 1) as f32 - } - - /// Determine if the system appears to be idle - fn is_system_idle(&self) -> bool { - if self.cpu_usage_history.is_empty() { + if recent_log_count < 2 { return false; } - // System considered idle if the average CPU usage of last readings is below 10% - let recent_avg = - self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; - recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 - } + let recent_average = self + .cpu_log + .iter() + .rev() + .take(recent_log_count) + .map(|log| log.usage) + .sum::() + / recent_log_count as f64; - /// Calculate optimal polling interval based on system conditions - fn calculate_optimal_interval( - &self, - config: &AppConfig, - on_battery: bool, - ) -> anyhow::Result { - let params = IntervalParams { - base_interval: config.daemon.poll_interval_sec, - min_interval: config.daemon.min_poll_interval_sec, - max_interval: config.daemon.max_poll_interval_sec, - cpu_volatility: self.get_cpu_volatility(), - temp_volatility: self.get_temperature_volatility(), - battery_discharge_rate: self.battery_discharge_rate, - last_user_activity: self.last_user_activity.elapsed(), - is_system_idle: self.is_system_idle(), - on_battery, - }; - - compute_new(¶ms, self) + recent_average < 0.1 + && self + .cpu_volatility() + .is_none_or(|volatility| volatility.usage < 0.05) } } -/// 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) -> anyhow::Result<()> { - if min_interval < 1 { - bail!("min_interval must be ≥ 1"); - } - if max_interval < 1 { - bail!("max_interval must be ≥ 1"); - } - if max_interval >= min_interval { - Ok(()) - } else { - bail!( - "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" - ); +struct PowerSupplyLog { + at: Instant, + + /// Charge 0-1, as a percentage. + charge: f64, +} + +impl Daemon { + /// Calculates the discharge rate, returns a number between 0 and 1. + /// + /// The discharge rate is averaged per hour. + /// So a return value of Some(0.3) means the battery has been + /// discharging 30% per hour. + fn power_supply_discharge_rate(&self) -> Option { + let mut last_charge = None; + + // A list of increasing charge percentages. + let discharging: Vec<&PowerSupplyLog> = self + .power_supply_log + .iter() + .rev() + .take_while(move |log| { + let Some(last_charge_value) = last_charge else { + last_charge = Some(log.charge); + return true; + }; + + last_charge = Some(log.charge); + + log.charge > last_charge_value + }) + .collect(); + + if discharging.len() < 2 { + return None; + } + + // Start of discharging. Has the most charge. + let start = discharging.last().unwrap(); + // End of discharging, very close to now. Has the least charge. + let end = discharging.first().unwrap(); + + let discharging_duration_seconds = (start.at - end.at).as_secs_f64(); + let discharging_duration_hours = discharging_duration_seconds / 60.0 / 60.0; + let discharged = start.charge - end.charge; + + Some(discharged / discharging_duration_hours) } } -/// Run the daemon -pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { - log::info!("Starting superfreq daemon..."); +impl Daemon { + fn polling_interval(&self) -> Duration { + let mut interval = Duration::from_secs(5); - // Validate critical configuration values before proceeding - validate_poll_intervals( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - )?; - - // Create a flag that will be set to true when a signal is received - let running = Arc::new(AtomicBool::new(true)); - let r = running.clone(); - - // Set up signal handlers - ctrlc::set_handler(move || { - log::info!("Received shutdown signal, exiting..."); - r.store(false, Ordering::SeqCst); - }) - .context("failed to set Ctrl-C handler")?; - - 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 { - 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 { - log::warn!( - "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" - ); - } - let mut system_history = SystemHistory::default(); - - // Main loop - while running.load(Ordering::SeqCst) { - let start_time = Instant::now(); - - match monitor::collect_system_report(&config) { - Ok(report) => { - log::debug!("Collected system report, applying settings..."); - - // Store the current state before updating history - let previous_state = system_history.current_state.clone(); - - // Update system history with new data - system_history.update(&report); - - // 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) { - log::error!("Failed to write stats file: {e}"); - } - } - - match engine::determine_and_apply_settings(&report, &config, None) { - Ok(()) => { - log::debug!("Successfully applied system settings"); - - // If system state changed, log the new state - if system_history.current_state != previous_state { - log::info!( - "System state changed to: {:?}", - system_history.current_state - ); - } - } - Err(e) => { - log::error!("Error applying system settings: {e}"); - } - } - - // Check if we're on battery - let on_battery = !report.batteries.is_empty() - && report.batteries.first().is_some_and(|b| !b.ac_connected); - - // Calculate optimal polling interval if adaptive polling is enabled - if config.daemon.adaptive_interval { - match system_history.calculate_optimal_interval(&config, on_battery) { - Ok(optimal_interval) => { - // Store the new interval - system_history.last_computed_interval = Some(optimal_interval); - - log::debug!("Recalculated optimal interval: {optimal_interval}s"); - - // Don't change the interval too dramatically at once - match optimal_interval.cmp(¤t_poll_interval) { - std::cmp::Ordering::Greater => { - current_poll_interval = - (current_poll_interval + optimal_interval) / 2; - } - std::cmp::Ordering::Less => { - current_poll_interval = current_poll_interval - - ((current_poll_interval - optimal_interval) / 2).max(1); - } - std::cmp::Ordering::Equal => { - // No change needed when they're equal - } - } - } - Err(e) => { - // Log the error and stop the daemon when an invalid configuration is detected - log::error!("Critical configuration error: {e}"); - running.store(false, Ordering::SeqCst); - break; - } - } - - // Make sure that we respect the (user) configured min and max limits - current_poll_interval = current_poll_interval.clamp( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - ); - - 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 { - let battery_multiplier = 2; // poll half as often on battery - - // We need to make sure `poll_interval_sec` is *at least* 1 - // before multiplying. - let safe_interval = config.daemon.poll_interval_sec.max(1); - current_poll_interval = (safe_interval * battery_multiplier) - .min(config.daemon.max_poll_interval_sec); - - log::debug!( - "On battery power, increased poll interval to {current_poll_interval}s" - ); + // We are on battery, so we must be more conservative with our polling. + if !self.charging { + match self.power_supply_discharge_rate() { + Some(discharge_rate) => { + if discharge_rate > 0.2 { + interval *= 3; + } else if discharge_rate > 0.1 { + interval *= 2; } else { - // Use the configured poll interval - current_poll_interval = config.daemon.poll_interval_sec.max(1); - if config.daemon.poll_interval_sec == 0 { - log::debug!( - "Using minimum poll interval of 1s instead of configured 0s" - ); - } + // *= 1.5; + interval /= 2; + interval *= 3; } } - } - Err(e) => { - log::error!("Error collecting system report: {e}"); + + None => { + interval *= 2; + } } } - // Sleep for the remaining time in the poll interval - let elapsed = start_time.elapsed(); - let poll_duration = Duration::from_secs(current_poll_interval); - if elapsed < poll_duration { - let sleep_time = poll_duration - elapsed; - log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); - std::thread::sleep(sleep_time); + if self.is_cpu_idle() { + let idle_for = self.last_user_activity.elapsed(); + + if idle_for > Duration::from_secs(30) { + let factor = idle_multiplier(idle_for); + + log::debug!( + "system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x", + seconds = idle_for.as_secs(), + minutes = idle_for.as_secs() / 60, + ); + + interval = Duration::from_secs_f64(interval.as_secs_f64() * factor); + } } + + if let Some(volatility) = self.cpu_volatility() { + if volatility.usage > 0.1 || volatility.temperature > 0.02 { + interval = (interval / 2).max(Duration::from_secs(1)); + } + } + + todo!("implement rest from daemon_old.rs") } +} - log::info!("Daemon stopped"); +pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { Ok(()) } - -/// Write current system stats to a file for --stats to read -fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { - let mut file = File::create(path)?; - - writeln!(file, "timestamp={:?}", report.timestamp)?; - - // CPU info - writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; - writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; - if let Some(temp) = report.cpu_global.average_temperature_celsius { - writeln!(file, "cpu_temp={temp:.1}")?; - } - - // Battery info - if !report.batteries.is_empty() { - let battery = &report.batteries[0]; - writeln!(file, "ac_power={}", battery.ac_connected)?; - if let Some(cap) = battery.capacity_percent { - writeln!(file, "battery_percent={cap}")?; - } - } - - // System load - writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; - writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; - writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; - - Ok(()) -} - -/// Simplified system state used for determining when to adjust polling interval -#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] -enum SystemState { - #[default] - Unknown, - OnAC, - OnBattery, - HighLoad, - LowLoad, - HighTemp, - Idle, -} - -/// Determine the current system state for adaptive polling -fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { - // Check power state first - if !report.batteries.is_empty() { - if let Some(battery) = report.batteries.first() { - if battery.ac_connected { - return SystemState::OnAC; - } - return SystemState::OnBattery; - } - } - - // No batteries means desktop, so always AC - if report.batteries.is_empty() { - return SystemState::OnAC; - } - - // Check temperature - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if temp > 80.0 { - return SystemState::HighTemp; - } - } - - // Check load first, as high load should take precedence over idle state - let avg_load = report.system_load.load_avg_1min; - if avg_load > 3.0 { - return SystemState::HighLoad; - } - - // Check idle state only if we don't have high load - if history.is_system_idle() { - return SystemState::Idle; - } - - // Check for low load - if avg_load < 0.5 { - return SystemState::LowLoad; - } - - // Default case - SystemState::Unknown -} diff --git a/src/daemon_old.rs b/src/daemon_old.rs new file mode 100644 index 0000000..ba6d37d --- /dev/null +++ b/src/daemon_old.rs @@ -0,0 +1,649 @@ +use anyhow::Context; +use anyhow::bail; + +use crate::config::AppConfig; +use crate::core::SystemReport; +use crate::engine; +use crate::monitor; +use std::collections::VecDeque; +use std::fs::File; +use std::io::Write; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +/// Parameters for computing optimal polling interval +struct IntervalParams { + /// Base polling interval in seconds + base_interval: u64, + /// Minimum allowed polling interval in seconds + min_interval: u64, + /// Maximum allowed polling interval in seconds + max_interval: u64, + /// How rapidly CPU usage is changing + cpu_volatility: f32, + /// How rapidly temperature is changing + temp_volatility: f32, + /// Battery discharge rate in %/hour if available + battery_discharge_rate: Option, + /// Time since last detected user activity + last_user_activity: Duration, + /// Whether the system appears to be idle + is_system_idle: bool, + /// Whether the system is running on battery power + on_battery: bool, +} + +/// Calculate the idle time multiplier based on system idle duration +/// +/// Returns a multiplier between 1.0 and 5.0 (capped): +/// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 +/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) +fn idle_multiplier(idle_secs: u64) -> f32 { + if idle_secs == 0 { + return 1.0; // No idle time, no multiplier effect + } + + let idle_factor = if idle_secs < 120 { + // Less than 2 minutes (0 to 119 seconds) + // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) + 1.0 + (idle_secs as f32) / 120.0 + } else { + // 2 minutes (120 seconds) or more + let idle_time_minutes = idle_secs / 60; + // Logarithmic scaling: 1.0 + log2(minutes) + 1.0 + (idle_time_minutes as f32).log2().max(0.5) + }; + + // Cap the multiplier to avoid excessive intervals + idle_factor.min(5.0) // max factor of 5x +} + +/// 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) -> anyhow::Result { + // Use the centralized validation function + validate_poll_intervals(params.min_interval, params.max_interval)?; + + // Start with base interval + let mut adjusted_interval = params.base_interval; + + // If we're on battery, we want to be more aggressive about saving power + if params.on_battery { + // Apply a multiplier based on battery discharge rate + if let Some(discharge_rate) = params.battery_discharge_rate { + if discharge_rate > 20.0 { + // High discharge rate - increase polling interval significantly (3x) + adjusted_interval = adjusted_interval.saturating_mul(3); + } else if discharge_rate > 10.0 { + // Moderate discharge - double polling interval (2x) + adjusted_interval = adjusted_interval.saturating_mul(2); + } else { + // Low discharge rate - increase by 50% (multiply by 3/2) + adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); + } + } else { + // If we don't know discharge rate, use a conservative multiplier (2x) + adjusted_interval = adjusted_interval.saturating_mul(2); + } + } + + // Adjust for system idleness + if params.is_system_idle { + let idle_time_seconds = params.last_user_activity.as_secs(); + + // Apply adjustment only if the system has been idle for a non-zero duration + if idle_time_seconds > 0 { + let idle_factor = idle_multiplier(idle_time_seconds); + + log::debug!( + "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", + idle_time_seconds, + (idle_time_seconds as f32 / 60.0).round(), + idle_factor + ); + + // Convert f32 multiplier to integer-safe math + // Multiply by a large number first, then divide to maintain precision + // Use 1000 as the scaling factor to preserve up to 3 decimal places + let scaling_factor = 1000; + let scaled_factor = (idle_factor * scaling_factor as f32) as u64; + adjusted_interval = adjusted_interval + .saturating_mul(scaled_factor) + .saturating_div(scaling_factor); + } + // If idle_time_seconds is 0, no factor is applied by this block + } + + // Adjust for CPU/temperature volatility + if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { + // For division by 2 (halving the interval), we can safely use integer division + adjusted_interval = (adjusted_interval / 2).max(1); + } + + // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval + let min_safe_interval = params.min_interval.max(1); + let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); + + // Blend the new interval with the cached value if available + let blended_interval = if let Some(cached) = system_history.last_computed_interval { + // Use a weighted average: 70% previous value, 30% new value + // This smooths out drastic changes in polling frequency + const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% + const NEW_VALUE_WEIGHT: u128 = 3; // 30% + const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 + + // XXX: Use u128 arithmetic to avoid overflow with large interval values + let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT + + u128::from(new_interval) * NEW_VALUE_WEIGHT) + / TOTAL_WEIGHT; + + result as u64 + } else { + new_interval + }; + + // Blended result still needs to respect the configured bounds + // Again enforce minimum of 1 second regardless of params.min_interval + Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) +} + +/// Tracks historical system data for "advanced" adaptive polling +#[derive(Debug)] +struct SystemHistory { + /// Last several CPU usage measurements + cpu_usage_history: VecDeque, + /// Last several temperature readings + temperature_history: VecDeque, + /// Time of last detected user activity + last_user_activity: Instant, + /// Previous battery percentage (to calculate discharge rate) + last_battery_percentage: Option, + /// Timestamp of last battery reading + last_battery_timestamp: Option, + /// Battery discharge rate (%/hour) + battery_discharge_rate: Option, + /// Time spent in each system state + state_durations: std::collections::HashMap, + /// Last time a state transition happened + last_state_change: Instant, + /// Current system state + current_state: SystemState, + /// Last computed optimal polling interval + last_computed_interval: Option, +} + +impl Default for SystemHistory { + fn default() -> Self { + Self { + cpu_usage_history: VecDeque::new(), + temperature_history: VecDeque::new(), + last_user_activity: Instant::now(), + last_battery_percentage: None, + last_battery_timestamp: None, + battery_discharge_rate: None, + state_durations: std::collections::HashMap::new(), + last_state_change: Instant::now(), + current_state: SystemState::default(), + last_computed_interval: None, + } + } +} + +impl SystemHistory { + /// Update system history with new report data + fn update(&mut self, report: &SystemReport) { + // Update CPU usage history + if !report.cpu_cores.is_empty() { + let mut total_usage: f32 = 0.0; + let mut core_count: usize = 0; + + for core in &report.cpu_cores { + if let Some(usage) = core.usage_percent { + total_usage += usage; + core_count += 1; + } + } + + if core_count > 0 { + let avg_usage = total_usage / core_count as f32; + + // Keep only the last 5 measurements + if self.cpu_usage_history.len() >= 5 { + self.cpu_usage_history.pop_front(); + } + self.cpu_usage_history.push_back(avg_usage); + + // Update last_user_activity if CPU usage indicates activity + // Consider significant CPU usage or sudden change as user activity + if avg_usage > 20.0 + || (self.cpu_usage_history.len() > 1 + && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) + .abs() + > 15.0) + { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on CPU usage"); + } + } + } + + // Update temperature history + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if self.temperature_history.len() >= 5 { + self.temperature_history.pop_front(); + } + self.temperature_history.push_back(temp); + + // Significant temperature increase can indicate user activity + if self.temperature_history.len() > 1 { + let temp_change = + temp - self.temperature_history[self.temperature_history.len() - 2]; + if temp_change > 5.0 { + // 5°C rise in temperature + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on temperature change"); + } + } + } + + // Update battery discharge rate + if let Some(battery) = report.batteries.first() { + // Reset when we are charging or have just connected AC + if battery.ac_connected { + // Reset discharge tracking but continue updating the rest of + // the history so we still detect activity/load changes on AC. + self.battery_discharge_rate = None; + self.last_battery_percentage = None; + self.last_battery_timestamp = None; + } + + if let Some(current_percentage) = battery.capacity_percent { + let current_percent = f32::from(current_percentage); + + if let (Some(last_percentage), Some(last_timestamp)) = + (self.last_battery_percentage, self.last_battery_timestamp) + { + let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; + // Only calculate discharge rate if at least 30 seconds have passed + // and we're not on AC power + if elapsed_hours > 0.0083 && !battery.ac_connected { + // 0.0083 hours = 30 seconds + // Calculate discharge rate in percent per hour + let percent_change = last_percentage - current_percent; + if percent_change > 0.0 { + // Only if battery is discharging + let hourly_rate = percent_change / elapsed_hours; + // Clamp the discharge rate to a reasonable maximum value (100%/hour) + let clamped_rate = hourly_rate.min(100.0); + self.battery_discharge_rate = Some(clamped_rate); + } + } + } + + self.last_battery_percentage = Some(current_percent); + self.last_battery_timestamp = Some(Instant::now()); + } + } + + // Update system state tracking + let new_state = determine_system_state(report, self); + if new_state != self.current_state { + // Record time spent in previous state + let time_in_state = self.last_state_change.elapsed(); + *self + .state_durations + .entry(self.current_state.clone()) + .or_insert(Duration::ZERO) += time_in_state; + + // State changes (except to Idle) likely indicate user activity + if new_state != SystemState::Idle && new_state != SystemState::LowLoad { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on system state change to {new_state:?}"); + } + + // Update state + self.current_state = new_state; + self.last_state_change = Instant::now(); + } + + // Check for significant load changes + if report.system_load.load_avg_1min > 1.0 { + self.last_user_activity = Instant::now(); + log::debug!("User activity detected based on system load"); + } + } + + /// Calculate CPU usage volatility (how much it's changing) + fn get_cpu_volatility(&self) -> f32 { + if self.cpu_usage_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.cpu_usage_history.len() { + sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); + } + + sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 + } + + /// Calculate temperature volatility + fn get_temperature_volatility(&self) -> f32 { + if self.temperature_history.len() < 2 { + return 0.0; + } + + let mut sum_of_changes = 0.0; + for i in 1..self.temperature_history.len() { + sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); + } + + sum_of_changes / (self.temperature_history.len() - 1) as f32 + } + + /// Determine if the system appears to be idle + fn is_system_idle(&self) -> bool { + if self.cpu_usage_history.is_empty() { + return false; + } + + // System considered idle if the average CPU usage of last readings is below 10% + let recent_avg = + self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; + recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 + } + + /// Calculate optimal polling interval based on system conditions + fn calculate_optimal_interval( + &self, + config: &AppConfig, + on_battery: bool, + ) -> anyhow::Result { + let params = IntervalParams { + base_interval: config.daemon.poll_interval_sec, + min_interval: config.daemon.min_poll_interval_sec, + max_interval: config.daemon.max_poll_interval_sec, + cpu_volatility: self.get_cpu_volatility(), + temp_volatility: self.get_temperature_volatility(), + battery_discharge_rate: self.battery_discharge_rate, + last_user_activity: self.last_user_activity.elapsed(), + is_system_idle: self.is_system_idle(), + on_battery, + }; + + compute_new(¶ms, self) + } +} + +/// 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) -> anyhow::Result<()> { + if min_interval < 1 { + bail!("min_interval must be ≥ 1"); + } + if max_interval < 1 { + bail!("max_interval must be ≥ 1"); + } + if max_interval >= min_interval { + Ok(()) + } else { + bail!( + "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" + ); + } +} + +/// Run the daemon +pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { + log::info!("Starting superfreq daemon..."); + + // Validate critical configuration values before proceeding + validate_poll_intervals( + config.daemon.min_poll_interval_sec, + config.daemon.max_poll_interval_sec, + )?; + + // Create a flag that will be set to true when a signal is received + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + // Set up signal handlers + ctrlc::set_handler(move || { + log::info!("Received shutdown signal, exiting..."); + r.store(false, Ordering::SeqCst); + }) + .context("failed to set Ctrl-C handler")?; + + 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 { + 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 { + log::warn!( + "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" + ); + } + let mut system_history = SystemHistory::default(); + + // Main loop + while running.load(Ordering::SeqCst) { + let start_time = Instant::now(); + + match monitor::collect_system_report(&config) { + Ok(report) => { + log::debug!("Collected system report, applying settings..."); + + // Store the current state before updating history + let previous_state = system_history.current_state.clone(); + + // Update system history with new data + system_history.update(&report); + + // 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) { + log::error!("Failed to write stats file: {e}"); + } + } + + match engine::determine_and_apply_settings(&report, &config, None) { + Ok(()) => { + log::debug!("Successfully applied system settings"); + + // If system state changed, log the new state + if system_history.current_state != previous_state { + log::info!( + "System state changed to: {:?}", + system_history.current_state + ); + } + } + Err(e) => { + log::error!("Error applying system settings: {e}"); + } + } + + // Check if we're on battery + let on_battery = !report.batteries.is_empty() + && report.batteries.first().is_some_and(|b| !b.ac_connected); + + // Calculate optimal polling interval if adaptive polling is enabled + if config.daemon.adaptive_interval { + match system_history.calculate_optimal_interval(&config, on_battery) { + Ok(optimal_interval) => { + // Store the new interval + system_history.last_computed_interval = Some(optimal_interval); + + log::debug!("Recalculated optimal interval: {optimal_interval}s"); + + // Don't change the interval too dramatically at once + match optimal_interval.cmp(¤t_poll_interval) { + std::cmp::Ordering::Greater => { + current_poll_interval = + (current_poll_interval + optimal_interval) / 2; + } + std::cmp::Ordering::Less => { + current_poll_interval = current_poll_interval + - ((current_poll_interval - optimal_interval) / 2).max(1); + } + std::cmp::Ordering::Equal => { + // No change needed when they're equal + } + } + } + Err(e) => { + // Log the error and stop the daemon when an invalid configuration is detected + log::error!("Critical configuration error: {e}"); + running.store(false, Ordering::SeqCst); + break; + } + } + + // Make sure that we respect the (user) configured min and max limits + current_poll_interval = current_poll_interval.clamp( + config.daemon.min_poll_interval_sec, + config.daemon.max_poll_interval_sec, + ); + + 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 { + let battery_multiplier = 2; // poll half as often on battery + + // We need to make sure `poll_interval_sec` is *at least* 1 + // before multiplying. + let safe_interval = config.daemon.poll_interval_sec.max(1); + current_poll_interval = (safe_interval * battery_multiplier) + .min(config.daemon.max_poll_interval_sec); + + 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 { + log::debug!( + "Using minimum poll interval of 1s instead of configured 0s" + ); + } + } + } + } + Err(e) => { + log::error!("Error collecting system report: {e}"); + } + } + + // Sleep for the remaining time in the poll interval + let elapsed = start_time.elapsed(); + let poll_duration = Duration::from_secs(current_poll_interval); + if elapsed < poll_duration { + let sleep_time = poll_duration - elapsed; + log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); + std::thread::sleep(sleep_time); + } + } + + log::info!("Daemon stopped"); + Ok(()) +} + +/// Write current system stats to a file for --stats to read +fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { + let mut file = File::create(path)?; + + writeln!(file, "timestamp={:?}", report.timestamp)?; + + // CPU info + writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; + writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; + if let Some(temp) = report.cpu_global.average_temperature_celsius { + writeln!(file, "cpu_temp={temp:.1}")?; + } + + // Battery info + if !report.batteries.is_empty() { + let battery = &report.batteries[0]; + writeln!(file, "ac_power={}", battery.ac_connected)?; + if let Some(cap) = battery.capacity_percent { + writeln!(file, "battery_percent={cap}")?; + } + } + + // System load + writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; + writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; + writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; + + Ok(()) +} + +/// Simplified system state used for determining when to adjust polling interval +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] +enum SystemState { + #[default] + Unknown, + OnAC, + OnBattery, + HighLoad, + LowLoad, + HighTemp, + Idle, +} + +/// Determine the current system state for adaptive polling +fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { + // Check power state first + if !report.batteries.is_empty() { + if let Some(battery) = report.batteries.first() { + if battery.ac_connected { + return SystemState::OnAC; + } + return SystemState::OnBattery; + } + } + + // No batteries means desktop, so always AC + if report.batteries.is_empty() { + return SystemState::OnAC; + } + + // Check temperature + if let Some(temp) = report.cpu_global.average_temperature_celsius { + if temp > 80.0 { + return SystemState::HighTemp; + } + } + + // Check load first, as high load should take precedence over idle state + let avg_load = report.system_load.load_avg_1min; + if avg_load > 3.0 { + return SystemState::HighLoad; + } + + // Check idle state only if we don't have high load + if history.is_system_idle() { + return SystemState::Idle; + } + + // Check for low load + if avg_load < 0.5 { + return SystemState::LowLoad; + } + + // Default case + SystemState::Unknown +} diff --git a/src/main.rs b/src/main.rs index 52f33c9..0725e38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod config; // mod core; mod cpu; -// mod daemon; +mod daemon; // mod engine; // mod monitor; mod power_supply; @@ -56,11 +56,10 @@ fn real_main() -> anyhow::Result<()> { Command::Info => todo!(), Command::Start { config } => { - let _config = config::DaemonConfig::load_from(&config) + let config = config::DaemonConfig::load_from(&config) .context("failed to load daemon config file")?; - // daemon::run(config) - Ok(()) + daemon::run(config) } Command::CpuSet(delta) => delta.apply(), From 4fa59b7ed4237f9156f1978416798323b27767cd Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 18:41:02 +0300 Subject: [PATCH 62/93] daemon: implement polling_interval --- src/daemon.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 55241a5..6275426 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -39,7 +39,11 @@ struct Daemon { /// Power supply status log. power_supply_log: VecDeque, + /// Whether if we are charging right now. charging: bool, + + /// The last computed polling interval. + last_polling_interval: Option, } struct CpuLog { @@ -167,7 +171,7 @@ impl Daemon { } impl Daemon { - fn polling_interval(&self) -> Duration { + fn polling_interval(&mut self) -> Duration { let mut interval = Duration::from_secs(5); // We are on battery, so we must be more conservative with our polling. @@ -185,6 +189,8 @@ impl Daemon { } } + // If we can't deterine the discharge rate, that means that + // we were very recently started. Which is user activity. None => { interval *= 2; } @@ -213,7 +219,20 @@ impl Daemon { } } - todo!("implement rest from daemon_old.rs") + let interval = match self.last_polling_interval { + Some(last_interval) => Duration::from_secs_f64( + // 30% of current computed interval, 70% of last interval. + interval.as_secs_f64() * 0.3 + last_interval.as_secs_f64() * 0.7, + ), + + None => interval, + }; + + let interval = Duration::from_secs_f64(interval.as_secs_f64().clamp(1.0, 30.0)); + + self.last_polling_interval = Some(interval); + + interval } } From c0623274576bd7da56a54138519437de444e009e Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 18:55:06 +0300 Subject: [PATCH 63/93] daemon: delete some old code and create daemon scaffold --- config.toml | 2 + src/config.rs | 4 +- src/daemon.rs | 33 +++++-- src/daemon_old.rs | 223 ---------------------------------------------- 4 files changed, 31 insertions(+), 231 deletions(-) create mode 100644 config.toml diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..2f796b7 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +[[rule]] +priority = 0 diff --git a/src/config.rs b/src/config.rs index 585b7a7..9ca0498 100644 --- a/src/config.rs +++ b/src/config.rs @@ -255,7 +255,7 @@ pub enum Expression { pub struct Rule { priority: u8, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(default, rename = "if", skip_serializing_if = "is_default")] if_: Expression, #[serde(default, skip_serializing_if = "is_default")] @@ -265,7 +265,7 @@ pub struct Rule { } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] -#[serde(transparent, default, rename_all = "kebab-case")] +#[serde(default, rename_all = "kebab-case")] pub struct DaemonConfig { #[serde(rename = "rule")] rules: Vec, diff --git a/src/daemon.rs b/src/daemon.rs index 6275426..f2d2e3a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,9 +1,15 @@ use std::{ collections::VecDeque, ops, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, time::{Duration, Instant}, }; +use anyhow::Context; + use crate::config; /// Calculate the idle time multiplier based on system idle time. @@ -33,17 +39,17 @@ struct Daemon { /// Last time when there was user activity. last_user_activity: Instant, + /// The last computed polling interval. + last_polling_interval: Option, + + /// Whether if we are charging right now. + charging: bool, + /// CPU usage and temperature log. cpu_log: VecDeque, /// Power supply status log. power_supply_log: VecDeque, - - /// Whether if we are charging right now. - charging: bool, - - /// The last computed polling interval. - last_polling_interval: Option, } struct CpuLog { @@ -237,5 +243,20 @@ impl Daemon { } pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { + log::info!("starting daemon..."); + + let cancelled = Arc::new(AtomicBool::new(false)); + + let cancelled_ = Arc::clone(&cancelled); + ctrlc::set_handler(move || { + log::info!("received shutdown signal"); + cancelled_.store(true, Ordering::SeqCst); + }) + .context("failed to set Ctrl-C handler")?; + + while !cancelled.load(Ordering::SeqCst) {} + + log::info!("exiting..."); + Ok(()) } diff --git a/src/daemon_old.rs b/src/daemon_old.rs index ba6d37d..3a20cb4 100644 --- a/src/daemon_old.rs +++ b/src/daemon_old.rs @@ -12,143 +12,6 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -/// Parameters for computing optimal polling interval -struct IntervalParams { - /// Base polling interval in seconds - base_interval: u64, - /// Minimum allowed polling interval in seconds - min_interval: u64, - /// Maximum allowed polling interval in seconds - max_interval: u64, - /// How rapidly CPU usage is changing - cpu_volatility: f32, - /// How rapidly temperature is changing - temp_volatility: f32, - /// Battery discharge rate in %/hour if available - battery_discharge_rate: Option, - /// Time since last detected user activity - last_user_activity: Duration, - /// Whether the system appears to be idle - is_system_idle: bool, - /// Whether the system is running on battery power - on_battery: bool, -} - -/// Calculate the idle time multiplier based on system idle duration -/// -/// Returns a multiplier between 1.0 and 5.0 (capped): -/// - For idle times < 2 minutes: Linear interpolation from 1.0 to 2.0 -/// - For idle times >= 2 minutes: Logarithmic scaling (1.0 + log2(minutes)) -fn idle_multiplier(idle_secs: u64) -> f32 { - if idle_secs == 0 { - return 1.0; // No idle time, no multiplier effect - } - - let idle_factor = if idle_secs < 120 { - // Less than 2 minutes (0 to 119 seconds) - // Linear interpolation from 1.0 (at 0s) to 2.0 (at 120s) - 1.0 + (idle_secs as f32) / 120.0 - } else { - // 2 minutes (120 seconds) or more - let idle_time_minutes = idle_secs / 60; - // Logarithmic scaling: 1.0 + log2(minutes) - 1.0 + (idle_time_minutes as f32).log2().max(0.5) - }; - - // Cap the multiplier to avoid excessive intervals - idle_factor.min(5.0) // max factor of 5x -} - -/// 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) -> anyhow::Result { - // Use the centralized validation function - validate_poll_intervals(params.min_interval, params.max_interval)?; - - // Start with base interval - let mut adjusted_interval = params.base_interval; - - // If we're on battery, we want to be more aggressive about saving power - if params.on_battery { - // Apply a multiplier based on battery discharge rate - if let Some(discharge_rate) = params.battery_discharge_rate { - if discharge_rate > 20.0 { - // High discharge rate - increase polling interval significantly (3x) - adjusted_interval = adjusted_interval.saturating_mul(3); - } else if discharge_rate > 10.0 { - // Moderate discharge - double polling interval (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } else { - // Low discharge rate - increase by 50% (multiply by 3/2) - adjusted_interval = adjusted_interval.saturating_mul(3).saturating_div(2); - } - } else { - // If we don't know discharge rate, use a conservative multiplier (2x) - adjusted_interval = adjusted_interval.saturating_mul(2); - } - } - - // Adjust for system idleness - if params.is_system_idle { - let idle_time_seconds = params.last_user_activity.as_secs(); - - // Apply adjustment only if the system has been idle for a non-zero duration - if idle_time_seconds > 0 { - let idle_factor = idle_multiplier(idle_time_seconds); - - log::debug!( - "System idle for {} seconds (approx. {} minutes), applying idle factor: {:.2}x", - idle_time_seconds, - (idle_time_seconds as f32 / 60.0).round(), - idle_factor - ); - - // Convert f32 multiplier to integer-safe math - // Multiply by a large number first, then divide to maintain precision - // Use 1000 as the scaling factor to preserve up to 3 decimal places - let scaling_factor = 1000; - let scaled_factor = (idle_factor * scaling_factor as f32) as u64; - adjusted_interval = adjusted_interval - .saturating_mul(scaled_factor) - .saturating_div(scaling_factor); - } - // If idle_time_seconds is 0, no factor is applied by this block - } - - // Adjust for CPU/temperature volatility - if params.cpu_volatility > 10.0 || params.temp_volatility > 2.0 { - // For division by 2 (halving the interval), we can safely use integer division - adjusted_interval = (adjusted_interval / 2).max(1); - } - - // Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval - let min_safe_interval = params.min_interval.max(1); - let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval); - - // Blend the new interval with the cached value if available - let blended_interval = if let Some(cached) = system_history.last_computed_interval { - // Use a weighted average: 70% previous value, 30% new value - // This smooths out drastic changes in polling frequency - const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70% - const NEW_VALUE_WEIGHT: u128 = 3; // 30% - const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10 - - // XXX: Use u128 arithmetic to avoid overflow with large interval values - let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT - + u128::from(new_interval) * NEW_VALUE_WEIGHT) - / TOTAL_WEIGHT; - - result as u64 - } else { - new_interval - }; - - // Blended result still needs to respect the configured bounds - // Again enforce minimum of 1 second regardless of params.min_interval - Ok(blended_interval.clamp(min_safe_interval, params.max_interval)) -} - /// Tracks historical system data for "advanced" adaptive polling #[derive(Debug)] struct SystemHistory { @@ -174,23 +37,6 @@ struct SystemHistory { last_computed_interval: Option, } -impl Default for SystemHistory { - fn default() -> Self { - Self { - cpu_usage_history: VecDeque::new(), - temperature_history: VecDeque::new(), - last_user_activity: Instant::now(), - last_battery_percentage: None, - last_battery_timestamp: None, - battery_discharge_rate: None, - state_durations: std::collections::HashMap::new(), - last_state_change: Instant::now(), - current_state: SystemState::default(), - last_computed_interval: None, - } - } -} - impl SystemHistory { /// Update system history with new report data fn update(&mut self, report: &SystemReport) { @@ -354,45 +200,6 @@ impl SystemHistory { self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 } - - /// Calculate optimal polling interval based on system conditions - fn calculate_optimal_interval( - &self, - config: &AppConfig, - on_battery: bool, - ) -> anyhow::Result { - let params = IntervalParams { - base_interval: config.daemon.poll_interval_sec, - min_interval: config.daemon.min_poll_interval_sec, - max_interval: config.daemon.max_poll_interval_sec, - cpu_volatility: self.get_cpu_volatility(), - temp_volatility: self.get_temperature_volatility(), - battery_discharge_rate: self.battery_discharge_rate, - last_user_activity: self.last_user_activity.elapsed(), - is_system_idle: self.is_system_idle(), - on_battery, - }; - - compute_new(¶ms, self) - } -} - -/// 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) -> anyhow::Result<()> { - if min_interval < 1 { - bail!("min_interval must be ≥ 1"); - } - if max_interval < 1 { - bail!("max_interval must be ≥ 1"); - } - if max_interval >= min_interval { - Ok(()) - } else { - bail!( - "Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})" - ); - } } /// Run the daemon @@ -561,36 +368,6 @@ pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { Ok(()) } -/// Write current system stats to a file for --stats to read -fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Error> { - let mut file = File::create(path)?; - - writeln!(file, "timestamp={:?}", report.timestamp)?; - - // CPU info - writeln!(file, "governor={:?}", report.cpu_global.current_governor)?; - writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; - if let Some(temp) = report.cpu_global.average_temperature_celsius { - writeln!(file, "cpu_temp={temp:.1}")?; - } - - // Battery info - if !report.batteries.is_empty() { - let battery = &report.batteries[0]; - writeln!(file, "ac_power={}", battery.ac_connected)?; - if let Some(cap) = battery.capacity_percent { - writeln!(file, "battery_percent={cap}")?; - } - } - - // System load - writeln!(file, "load_1m={:.2}", report.system_load.load_avg_1min)?; - writeln!(file, "load_5m={:.2}", report.system_load.load_avg_5min)?; - writeln!(file, "load_15m={:.2}", report.system_load.load_avg_15min)?; - - Ok(()) -} - /// Simplified system state used for determining when to adjust polling interval #[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] enum SystemState { From 91cef3b8b1bf9979e467e5368c3530f5bfaf75b3 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:10:10 +0300 Subject: [PATCH 64/93] cpu&power: share fs impls --- src/cpu.rs | 60 ++++++++++++++------------------------------- src/fs.rs | 55 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 8 ++++-- src/power_supply.rs | 32 +++++++++--------------- src/system.rs | 14 +++++++++++ 5 files changed, 104 insertions(+), 65 deletions(-) create mode 100644 src/fs.rs create mode 100644 src/system.rs diff --git a/src/cpu.rs b/src/cpu.rs index 17a5da1..7b8ef99 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,33 +1,9 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{fmt, fs, path::Path, string::ToString}; +use std::{fmt, string::ToString}; -fn exists(path: impl AsRef) -> bool { - let path = path.as_ref(); - - path.exists() -} - -// 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(); - - 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(), - ) - }) -} +use crate::fs; #[derive(Debug, Clone, Copy)] pub struct Cpu { @@ -96,11 +72,11 @@ impl Cpu { pub fn rescan(&mut self) -> anyhow::Result<()> { let Self { number, .. } = self; - if !exists(format!("/sys/devices/system/cpu/cpu{number}")) { + if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { bail!("{self} does not exist"); } - let has_cpufreq = exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); + let has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq = has_cpufreq; @@ -110,7 +86,7 @@ impl Cpu { pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; - let Ok(content) = fs::read_to_string(format!( + let Ok(Some(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" )) else { return Vec::new(); @@ -137,7 +113,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"), governor, ) @@ -151,7 +127,7 @@ impl Cpu { pub fn get_available_epps(&self) -> Vec { let Self { number, .. } = self; - let Ok(content) = fs::read_to_string(format!( + let Ok(Some(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" )) else { return Vec::new(); @@ -175,7 +151,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), epp, ) @@ -226,7 +202,7 @@ impl Cpu { ); } - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), epb, ) @@ -244,7 +220,7 @@ impl Cpu { let frequency_khz = frequency_mhz * 1000; let frequency_khz = frequency_khz.to_string(); - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"), &frequency_khz, ) @@ -256,7 +232,7 @@ impl Cpu { fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(minimum_frequency_khz) = read_u64(format!( + let Ok(minimum_frequency_khz) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -282,7 +258,7 @@ impl Cpu { let frequency_khz = frequency_mhz * 1000; let frequency_khz = frequency_khz.to_string(); - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"), &frequency_khz, ) @@ -294,7 +270,7 @@ impl Cpu { fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(maximum_frequency_khz) = read_u64(format!( + let Ok(maximum_frequency_khz) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -331,16 +307,16 @@ impl Cpu { 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() { + if fs::write(intel_boost_path_negated, value_boost_negated).is_ok() { return Ok(()); } - if write(amd_boost_path, value_boost).is_ok() { + if fs::write(amd_boost_path, value_boost).is_ok() { return Ok(()); } - if write(msr_boost_path, value_boost).is_ok() { + if fs::write(msr_boost_path, value_boost).is_ok() { return Ok(()); } - if write(generic_boost_path, value_boost).is_ok() { + if fs::write(generic_boost_path, value_boost).is_ok() { return Ok(()); } @@ -348,7 +324,7 @@ impl Cpu { if Self::all()?.iter().any(|cpu| { let Cpu { number, .. } = cpu; - write( + fs::write( format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"), value_boost, ) diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..f3eeb2c --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,55 @@ +use std::{fs, io, path::Path}; + +use anyhow::Context; + +pub fn exists(path: impl AsRef) -> bool { + let path = path.as_ref(); + + path.exists() +} + +pub fn read_dir(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + + fs::read_dir(path) + .with_context(|| format!("failed to read directory '{path}'", path = path.display())) +} + +pub fn read(path: impl AsRef) -> anyhow::Result> { + let path = path.as_ref(); + + match fs::read_to_string(path) { + Ok(string) => Ok(Some(string)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => { + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) + } + } +} + +pub fn read_u64(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + + let content = fs::read_to_string(path) + .with_context(|| format!("failed to read '{path}'", path = path.display()))?; + + Ok(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })?) +} + +pub 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(), + ) + }) +} diff --git a/src/main.rs b/src/main.rs index 0725e38..825465d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,14 @@ +mod cpu; +mod power_supply; +mod system; + +mod fs; + mod config; // mod core; -mod cpu; mod daemon; // mod engine; // mod monitor; -mod power_supply; use anyhow::Context; use clap::Parser as _; diff --git a/src/power_supply.rs b/src/power_supply.rs index 1f69a3c..4e66bec 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -1,22 +1,12 @@ -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; use yansi::Paint as _; use std::{ - fmt, fs, + fmt, 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(), - ) - }) -} +use crate::fs; /// Represents a pattern of path suffixes used to control charge thresholds /// for different device vendors. @@ -136,10 +126,10 @@ impl PowerSupply { fn get_type(&self) -> anyhow::Result { let type_path = self.path.join("type"); - let type_ = fs::read_to_string(&type_path) + let type_ = fs::read(&type_path) .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - Ok(type_) + type_.ok_or_else(|| anyhow!("'{path}' doesn't exist", path = type_path.display())) } pub fn rescan(&mut self) -> anyhow::Result<()> { @@ -180,9 +170,9 @@ impl PowerSupply { } pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { - write( + fs::write( &self.charge_threshold_path_start().ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "power supply '{name}' does not support changing charge threshold levels", name = self.name, ) @@ -197,9 +187,9 @@ impl PowerSupply { } pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { - write( + fs::write( &self.charge_threshold_path_end().ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "power supply '{name}' does not support changing charge threshold levels", name = self.name, ) @@ -216,7 +206,7 @@ impl PowerSupply { pub fn get_available_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Ok(content) = fs::read_to_string(path) else { + let Ok(Some(content)) = fs::read(path) else { return Vec::new(); }; @@ -245,7 +235,7 @@ impl PowerSupply { ); } - write("/sys/firmware/acpi/platform_profile", profile) + fs::write("/sys/firmware/acpi/platform_profile", profile) .context("this probably means that your system does not support changing ACPI profiles") } } diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..1d3e697 --- /dev/null +++ b/src/system.rs @@ -0,0 +1,14 @@ +pub struct System { + pub is_desktop: bool, +} + +impl System { + pub fn new() -> anyhow::Result { + let mut system = Self { is_desktop: false }; + system.rescan()?; + + Ok(system) + } + + pub fn rescan(&mut self) -> anyhow::Result<()> {} +} From 543e5a052e659baafbfe4d783224e1cd79830137 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:12:58 +0300 Subject: [PATCH 65/93] fs: fix read() typesig --- src/cpu.rs | 4 ++-- src/fs.rs | 12 ++++++------ src/power_supply.rs | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 7b8ef99..0179746 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -86,7 +86,7 @@ impl Cpu { pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; - let Ok(Some(content)) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" )) else { return Vec::new(); @@ -127,7 +127,7 @@ impl Cpu { pub fn get_available_epps(&self) -> Vec { let Self { number, .. } = self; - let Ok(Some(content)) = fs::read(format!( + let Some(Ok(content)) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" )) else { return Vec::new(); diff --git a/src/fs.rs b/src/fs.rs index f3eeb2c..b1d1c71 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -15,17 +15,17 @@ pub fn read_dir(path: impl AsRef) -> anyhow::Result { .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } -pub fn read(path: impl AsRef) -> anyhow::Result> { +pub fn read(path: impl AsRef) -> Option> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Ok(Some(string)), + Ok(string) => Some(Ok(string)), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + Err(error) if error.kind() == io::ErrorKind::NotFound => None, - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Some( + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), + ), } } diff --git a/src/power_supply.rs b/src/power_supply.rs index 4e66bec..f1dcb41 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -127,9 +127,10 @@ impl PowerSupply { let type_path = self.path.join("type"); let type_ = fs::read(&type_path) + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - type_.ok_or_else(|| anyhow!("'{path}' doesn't exist", path = type_path.display())) + Ok(type_) } pub fn rescan(&mut self) -> anyhow::Result<()> { @@ -206,7 +207,7 @@ impl PowerSupply { pub fn get_available_platform_profiles() -> Vec { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Ok(Some(content)) = fs::read(path) else { + let Some(Ok(content)) = fs::read(path) else { return Vec::new(); }; From 137f801d2b4796018f797eb82b72b283e101efd0 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 20 May 2025 19:41:53 +0300 Subject: [PATCH 66/93] cpu&power: add more attributes --- src/fs.rs | 19 ++++---- src/main.rs | 2 +- src/monitor.rs | 104 -------------------------------------------- src/power_supply.rs | 48 ++++++++++++++++++++ src/system.rs | 98 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 116 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index b1d1c71..9b150b3 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -29,18 +29,19 @@ pub fn read(path: impl AsRef) -> Option> { } } -pub fn read_u64(path: impl AsRef) -> anyhow::Result { +pub fn read_u64(path: impl AsRef) -> Option> { let path = path.as_ref(); - let content = fs::read_to_string(path) - .with_context(|| format!("failed to read '{path}'", path = path.display()))?; + match read(path)? { + Ok(content) => Some(content.trim().parse().with_context(|| { + format!( + "failed to parse contents of '{path}' as a unsigned number", + path = path.display(), + ) + })), - Ok(content.trim().parse().with_context(|| { - format!( - "failed to parse contents of '{path}' as a unsigned number", - path = path.display(), - ) - })?) + Err(error) => Some(Err(error)), + } } pub fn write(path: impl AsRef, value: &str) -> anyhow::Result<()> { diff --git a/src/main.rs b/src/main.rs index 825465d..cd6258f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ enum Command { /// Start the daemon. Start { /// The daemon config path. - #[arg(long, env = "SUPERFREQ_CONFIG")] + #[arg(long, env = "WATT_CONFIG")] config: PathBuf, }, diff --git a/src/monitor.rs b/src/monitor.rs index 79d2635..cda52dc 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -599,110 +599,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> Ok(batteries) } -/// Check if a battery is likely a peripheral (mouse, keyboard, etc) not a laptop battery -fn is_peripheral_battery(ps_path: &Path, name: &str) -> bool { - // Convert name to lowercase once for case-insensitive matching - let name_lower = name.to_lowercase(); - - // Common peripheral battery names - if name_lower.contains("mouse") - || name_lower.contains("keyboard") - || name_lower.contains("trackpad") - || name_lower.contains("gamepad") - || name_lower.contains("controller") - || name_lower.contains("headset") - || name_lower.contains("headphone") - { - return true; - } - - // Small capacity batteries are likely not laptop batteries - if let Ok(energy_full) = read_sysfs_value::(ps_path.join("energy_full")) { - // Most laptop batteries are at least 20,000,000 µWh (20 Wh) - // Peripheral batteries are typically much smaller - if energy_full < 10_000_000 { - // 10 Wh in µWh - return true; - } - } - - // Check for model name that indicates a peripheral - if let Ok(model) = read_sysfs_file_trimmed(ps_path.join("model_name")) { - if model.contains("bluetooth") || model.contains("wireless") { - return true; - } - } - - false -} - -/// Determine if this is likely a desktop system rather than a laptop -fn is_likely_desktop_system() -> bool { - // Check for DMI system type information - if let Ok(chassis_type) = fs::read_to_string("/sys/class/dmi/id/chassis_type") { - let chassis_type = chassis_type.trim(); - - // Chassis types: - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One - // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis - match chassis_type { - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return true, // desktop form factors - "9" | "10" | "14" => return false, // laptop form factors - _ => {} // Unknown, continue with other checks - } - } - - // Check CPU power policies, desktops often don't have these - let power_saving_exists = Path::new("/sys/module/intel_pstate/parameters/no_hwp").exists() - || Path::new("/sys/devices/system/cpu/cpufreq/conservative").exists(); - - if !power_saving_exists { - return true; // likely a desktop - } - - // Check battery-specific ACPI paths that laptops typically have - let laptop_acpi_paths = [ - "/sys/class/power_supply/BAT0", - "/sys/class/power_supply/BAT1", - "/proc/acpi/battery", - ]; - - for path in &laptop_acpi_paths { - if Path::new(path).exists() { - return false; // Likely a laptop - } - } - - // Default to assuming desktop if we can't determine - true -} - -pub fn get_system_load() -> anyhow::Result { - let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; - let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); - if parts.len() < 3 { - return Err(SysMonitorError::ParseError( - "Could not parse /proc/loadavg: expected at least 3 parts".to_string(), - )); - } - let load_avg_1min = parts[0].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0])) - })?; - let load_avg_5min = parts[1].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1])) - })?; - let load_avg_15min = parts[2].parse().map_err(|_| { - SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2])) - })?; - - Ok(SystemLoad { - load_avg_1min, - load_avg_5min, - load_avg_15min, - }) -} - pub fn collect_system_report(config: &AppConfig) -> anyhow::Result { let system_info = get_system_info(); let cpu_cores = get_all_cpu_core_info()?; diff --git a/src/power_supply.rs b/src/power_supply.rs index f1dcb41..025ac75 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -48,6 +48,9 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ pub struct PowerSupply { pub name: String, pub path: PathBuf, + + pub is_from_peripheral: bool, + pub threshold_config: Option, } @@ -74,6 +77,9 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, + + is_from_peripheral: false, + threshold_config: None, }; @@ -94,6 +100,8 @@ impl PowerSupply { path, + is_from_peripheral: false, + threshold_config: None, }; @@ -157,6 +165,46 @@ impl PowerSupply { self.threshold_config = threshold_config; + self.is_from_peripheral = 'is_from_peripheral: { + let name_lower = self.name.to_lowercase(); + + // Common peripheral battery names. + if name_lower.contains("mouse") + || name_lower.contains("keyboard") + || name_lower.contains("trackpad") + || name_lower.contains("gamepad") + || name_lower.contains("controller") + || name_lower.contains("headset") + || name_lower.contains("headphone") + { + break 'is_from_peripheral true; + } + + // Small capacity batteries are likely not laptop batteries. + if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { + let energy_full = energy_full + .with_context(|| format!("failed to read the max charge '{self}' can hold"))?; + + // Most laptop batteries are at least 20,000,000 µWh (20 Wh). + // Peripheral batteries are typically much smaller. + if energy_full < 10_000_000 { + // 10 Wh in µWh. + break 'is_from_peripheral true; + } + } + // Check for model name that indicates a peripheral + if let Some(model) = fs::read(self.path.join("model_name")) { + let model = + model.with_context(|| format!("failed to read the model name of '{self}'"))?; + + if model.contains("bluetooth") || model.contains("wireless") { + break 'is_from_peripheral true; + } + } + + false + }; + Ok(()) } diff --git a/src/system.rs b/src/system.rs index 1d3e697..cc39776 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,14 +1,108 @@ +use anyhow::{Context, bail}; + +use crate::fs; + pub struct System { pub is_desktop: bool, + + pub load_average_1min: f64, + pub load_average_5min: f64, + pub load_average_15min: f64, } impl System { pub fn new() -> anyhow::Result { - let mut system = Self { is_desktop: false }; + let mut system = Self { + is_desktop: false, + + load_average_1min: 0.0, + load_average_5min: 0.0, + load_average_15min: 0.0, + }; + system.rescan()?; Ok(system) } - pub fn rescan(&mut self) -> anyhow::Result<()> {} + pub fn rescan(&mut self) -> anyhow::Result<()> { + self.is_desktop = self.is_desktop()?; + + let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?; + self.load_average_1min = load_average_1min; + self.load_average_5min = load_average_5min; + self.load_average_15min = load_average_15min; + + Ok(()) + } + + fn is_desktop(&self) -> anyhow::Result { + if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { + let chassis_type = chassis_type.context("failed to read chassis type")?; + + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis + match chassis_type.trim() { + // Desktop form factors. + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true), + // Laptop form factors. + "9" | "10" | "14" => return Ok(false), + // Unknown, continue with other checks + _ => {} + } + } + + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + return Ok(true); // Likely a desktop. + } + + // Check battery-specific ACPI paths that laptops typically have + let laptop_acpi_paths = [ + "/sys/class/power_supply/BAT0", + "/sys/class/power_supply/BAT1", + "/proc/acpi/battery", + ]; + + for path in laptop_acpi_paths { + if fs::exists(path) { + return Ok(false); // Likely a laptop. + } + } + + // Default to assuming desktop if we can't determine. + Ok(true) + } + + fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> { + let content = fs::read("/proc/loadavg") + .context("load average file doesn't exist, are you on linux?")? + .context("failed to read load average")?; + + let mut parts = content.split_whitespace(); + + let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) = + (parts.next(), parts.next(), parts.next()) + else { + bail!( + "failed to parse first 3 load average entries due to there not being enough, content: {content}" + ); + }; + + Ok(( + load_average_1min + .parse() + .context("failed to parse load average")?, + load_average_5min + .parse() + .context("failed to parse load average")?, + load_average_15min + .parse() + .context("failed to parse load average")?, + )) + } } From 1ab9aceced0e591151b36a21400e8264b3ff9f34 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:24:36 +0300 Subject: [PATCH 67/93] cpu: cpu times --- src/cpu.rs | 105 +++++++++++++++++++++++++++++++++++++++++--- src/monitor.rs | 105 -------------------------------------------- src/power_supply.rs | 4 +- src/system.rs | 54 +++++++++++++---------- 4 files changed, 131 insertions(+), 137 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 0179746..2e0db57 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,10 +5,37 @@ use std::{fmt, string::ToString}; use crate::fs; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Cpu { pub number: u32, + pub has_cpufreq: bool, + + pub time_user: u64, + pub time_nice: u64, + pub time_system: u64, + pub time_idle: u64, + pub time_iowait: u64, + pub time_irq: u64, + pub time_softirq: u64, + pub time_steal: u64, +} + +impl Cpu { + pub fn time_total(&self) -> u64 { + self.time_user + + self.time_nice + + self.time_system + + self.time_idle + + self.time_iowait + + self.time_irq + + self.time_softirq + + self.time_steal + } + + pub fn time_idle(&self) -> u64 { + self.time_idle + self.time_iowait + } } impl fmt::Display for Cpu { @@ -24,6 +51,15 @@ impl Cpu { let mut cpu = Self { number, has_cpufreq: false, + + time_user: 0, + time_nice: 0, + time_system: 0, + time_idle: 0, + time_iowait: 0, + time_irq: 0, + time_softirq: 0, + time_steal: 0, }; cpu.rescan()?; @@ -76,9 +112,68 @@ impl Cpu { bail!("{self} does not exist"); } - let has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); + self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.has_cpufreq = has_cpufreq; + self.rescan_times()?; + + Ok(()) + } + + fn rescan_times(&mut self) -> anyhow::Result<()> { + let content = fs::read("/proc/stat") + .context("/proc/stat does not exist")? + .context("failed to read CPU stat")?; + + let cpu_name = format!("cpu{number}", number = self.number); + + let mut stats = content + .lines() + .find_map(|line| { + line.starts_with(&cpu_name) + .then(|| line.split_whitespace().skip(1)) + }) + .with_context(|| format!("failed to find {self} in CPU stats"))?; + + self.time_user = stats + .next() + .with_context(|| format!("failed to find {self} user time"))? + .parse() + .with_context(|| format!("failed to parse {self} user time"))?; + self.time_nice = stats + .next() + .with_context(|| format!("failed to find {self} nice time"))? + .parse() + .with_context(|| format!("failed to parse {self} nice time"))?; + self.time_system = stats + .next() + .with_context(|| format!("failed to find {self} system time"))? + .parse() + .with_context(|| format!("failed to parse {self} system time"))?; + self.time_idle = stats + .next() + .with_context(|| format!("failed to find {self} idle time"))? + .parse() + .with_context(|| format!("failed to parse {self} idle time"))?; + self.time_iowait = stats + .next() + .with_context(|| format!("failed to find {self} iowait time"))? + .parse() + .with_context(|| format!("failed to parse {self} iowait time"))?; + self.time_irq = stats + .next() + .with_context(|| format!("failed to find {self} irq time"))? + .parse() + .with_context(|| format!("failed to parse {self} irq time"))?; + self.time_softirq = stats + .next() + .with_context(|| format!("failed to find {self} softirq time"))? + .parse() + .with_context(|| format!("failed to parse {self} softirq time"))?; + self.time_steal = stats + .next() + .with_context(|| format!("failed to find {self} steal time"))? + .parse() + .with_context(|| format!("failed to parse {self} steal time"))?; Ok(()) } @@ -232,7 +327,7 @@ impl Cpu { fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(minimum_frequency_khz) = fs::read_u64(format!( + let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. @@ -270,7 +365,7 @@ impl Cpu { fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Ok(maximum_frequency_khz) = fs::read_u64(format!( + let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) else { // Just let it pass if we can't find anything. diff --git a/src/monitor.rs b/src/monitor.rs index cda52dc..1cd0dc4 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -43,111 +43,6 @@ pub fn get_system_info() -> SystemInfo { } } -#[derive(Debug, Clone, Copy)] -pub struct CpuTimes { - user: u64, - nice: u64, - system: u64, - idle: u64, - iowait: u64, - irq: u64, - softirq: u64, - steal: u64, -} - -impl CpuTimes { - const fn total_time(&self) -> u64 { - self.user - + self.nice - + self.system - + self.idle - + self.iowait - + self.irq - + self.softirq - + self.steal - } - - const fn idle_time(&self) -> u64 { - self.idle + self.iowait - } -} - -fn read_all_cpu_times() -> anyhow::Result> { - let content = fs::read_to_string("/proc/stat").map_err(SysMonitorError::Io)?; - let mut cpu_times_map = HashMap::new(); - - for line in content.lines() { - if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() < 11 { - return Err(SysMonitorError::ProcStatParseError(format!( - "Line too short: {line}" - ))); - } - - let core_id_str = &parts[0][3..]; - let core_id = core_id_str.parse::().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse core_id: {core_id_str}" - )) - })?; - - let times = CpuTimes { - user: parts[1].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse user time: {}", - parts[1] - )) - })?, - nice: parts[2].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse nice time: {}", - parts[2] - )) - })?, - system: parts[3].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse system time: {}", - parts[3] - )) - })?, - idle: parts[4].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse idle time: {}", - parts[4] - )) - })?, - iowait: parts[5].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse iowait time: {}", - parts[5] - )) - })?, - irq: parts[6].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse irq time: {}", - parts[6] - )) - })?, - softirq: parts[7].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse softirq time: {}", - parts[7] - )) - })?, - steal: parts[8].parse().map_err(|_| { - SysMonitorError::ProcStatParseError(format!( - "Failed to parse steal time: {}", - parts[8] - )) - })?, - }; - cpu_times_map.insert(core_id, times); - } - } - Ok(cpu_times_map) -} - pub fn get_cpu_core_info( core_id: u32, prev_times: &CpuTimes, diff --git a/src/power_supply.rs b/src/power_supply.rs index 025ac75..5bbcebc 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -146,7 +146,7 @@ impl PowerSupply { bail!("{self} does not exist"); } - let threshold_config = self + self.threshold_config = self .get_type() .with_context(|| format!("failed to determine what type of power supply '{self}' is"))? .eq("Battery") @@ -163,8 +163,6 @@ impl PowerSupply { }) .flatten(); - self.threshold_config = threshold_config; - self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); diff --git a/src/system.rs b/src/system.rs index cc39776..f8820b1 100644 --- a/src/system.rs +++ b/src/system.rs @@ -26,17 +26,13 @@ impl System { } pub fn rescan(&mut self) -> anyhow::Result<()> { - self.is_desktop = self.is_desktop()?; - - let (load_average_1min, load_average_5min, load_average_15min) = self.load_average()?; - self.load_average_1min = load_average_1min; - self.load_average_5min = load_average_5min; - self.load_average_15min = load_average_15min; + self.rescan_is_desktop()?; + self.rescan_load_average()?; Ok(()) } - fn is_desktop(&self) -> anyhow::Result { + fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { let chassis_type = chassis_type.context("failed to read chassis type")?; @@ -45,9 +41,16 @@ impl System { // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis match chassis_type.trim() { // Desktop form factors. - "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => return Ok(true), + "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { + self.is_desktop = true; + return Ok(()); + } // Laptop form factors. - "9" | "10" | "14" => return Ok(false), + "9" | "10" | "14" => { + self.is_desktop = false; + return Ok(()); + } + // Unknown, continue with other checks _ => {} } @@ -58,7 +61,8 @@ impl System { || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); if !power_saving_exists { - return Ok(true); // Likely a desktop. + self.is_desktop = true; + return Ok(()); // Likely a desktop. } // Check battery-specific ACPI paths that laptops typically have @@ -70,15 +74,17 @@ impl System { for path in laptop_acpi_paths { if fs::exists(path) { - return Ok(false); // Likely a laptop. + self.is_desktop = false; // Likely a laptop. + return Ok(()); } } // Default to assuming desktop if we can't determine. - Ok(true) + self.is_desktop = true; + Ok(()) } - fn load_average(&self) -> anyhow::Result<(f64, f64, f64)> { + fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") .context("load average file doesn't exist, are you on linux?")? .context("failed to read load average")?; @@ -93,16 +99,16 @@ impl System { ); }; - Ok(( - load_average_1min - .parse() - .context("failed to parse load average")?, - load_average_5min - .parse() - .context("failed to parse load average")?, - load_average_15min - .parse() - .context("failed to parse load average")?, - )) + self.load_average_1min = load_average_1min + .parse() + .context("failed to parse load average")?; + self.load_average_5min = load_average_5min + .parse() + .context("failed to parse load average")?; + self.load_average_15min = load_average_15min + .parse() + .context("failed to parse load average")?; + + Ok(()) } } From 058ef997c6716c49ecf7d09cc495440b994b4565 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:28:06 +0300 Subject: [PATCH 68/93] cpu: add TODO --- src/cpu.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cpu.rs b/src/cpu.rs index 2e0db57..736008a 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -120,6 +120,8 @@ impl Cpu { } fn rescan_times(&mut self) -> anyhow::Result<()> { + // TODO: Don't read this per CPU. Share the read or + // find something in /sys/.../cpu{N} that does it. let content = fs::read("/proc/stat") .context("/proc/stat does not exist")? .context("failed to read CPU stat")?; From a514f1ba7a7ab7c18cbae5a0049472a27d5454ac Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 00:37:10 +0300 Subject: [PATCH 69/93] main: use yansi::whenever --- Cargo.lock | 22 +++++++++++++++++++++- Cargo.toml | 2 +- src/main.rs | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b0446d..f077741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + [[package]] name = "indexmap" version = "2.9.0" @@ -271,6 +277,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.1", + "libc", + "windows-sys", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -364,7 +381,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -697,3 +714,6 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] diff --git a/Cargo.toml b/Cargo.toml index 287929e..aeecd4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,5 @@ thiserror = "2.0" anyhow = "1.0" jiff = "0.2.13" clap-verbosity-flag = "3.0.2" -yansi = "1.0.1" +yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } derive_more = { version = "2.0.1", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index cd6258f..e435cee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,6 +50,8 @@ enum Command { fn real_main() -> anyhow::Result<()> { let cli = Cli::parse(); + yansi::whenever(yansi::Condition::TTY_AND_COLOR); + env_logger::Builder::new() .filter_level(cli.verbosity.log_level_filter()) .format_timestamp(None) From 230967832b595eb217f6bf62f2ac7172b18d91ce Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 01:01:45 +0300 Subject: [PATCH 70/93] monitor: delete old code --- src/core.rs | 2 -- src/monitor.rs | 71 +------------------------------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) diff --git a/src/core.rs b/src/core.rs index 07581aa..a3f4e33 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,8 +1,6 @@ pub struct SystemInfo { // Overall system details pub cpu_model: String, - pub architecture: String, - pub linux_distribution: String, } pub struct CpuCoreInfo { diff --git a/src/monitor.rs b/src/monitor.rs index 1cd0dc4..5d0468b 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -10,37 +10,10 @@ use std::{ time::SystemTime, }; -// Read a sysfs file to a string, trimming whitespace -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| { - SysMonitorError::ReadError(format!("Path: {:?}, Error: {}", path.as_ref().display(), e)) - }) -} - -// Read a sysfs file and parse it to a specific type -fn read_sysfs_value(path: impl AsRef) -> anyhow::Result { - let content = read_sysfs_file_trimmed(path.as_ref())?; - content.parse::().map_err(|_| { - SysMonitorError::ParseError(format!( - "Could not parse '{}' from {:?}", - content, - path.as_ref().display() - )) - }) -} - pub fn get_system_info() -> SystemInfo { let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string()); - let linux_distribution = get_linux_distribution().unwrap_or_else(|_| "Unknown".to_string()); - let architecture = std::env::consts::ARCH.to_string(); - SystemInfo { - cpu_model, - architecture, - linux_distribution, - } + SystemInfo { cpu_model } } pub fn get_cpu_core_info( @@ -529,45 +502,3 @@ pub fn get_cpu_model() -> anyhow::Result { "Could not find CPU model name in /proc/cpuinfo.".to_string(), )) } - -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!( - "Cannot read contents of {}.", - os_release_path.display() - )) - })?; - - for line in content.lines() { - if line.starts_with("PRETTY_NAME=") { - if let Some(val) = line.split('=').nth(1) { - let linux_distribution = val.trim_matches('"').to_string(); - return Ok(linux_distribution); - } - } - } - - let lsb_release_path = Path::new("/etc/lsb-release"); - let content = fs::read_to_string(lsb_release_path).map_err(|_| { - SysMonitorError::ReadError(format!( - "Cannot read contents of {}.", - lsb_release_path.display() - )) - })?; - - for line in content.lines() { - if line.starts_with("DISTRIB_DESCRIPTION=") { - if let Some(val) = line.split('=').nth(1) { - let linux_distribution = val.trim_matches('"').to_string(); - return Ok(linux_distribution); - } - } - } - - Err(SysMonitorError::ParseError(format!( - "Could not find distribution name in {} or {}.", - os_release_path.display(), - lsb_release_path.display() - ))) -} From d87237165b4cded6905c4128766b86d4b016c317 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 17:47:13 +0300 Subject: [PATCH 71/93] cpu: store frequency --- src/config.rs | 8 +++---- src/core.rs | 3 --- src/cpu.rs | 63 ++++++++++++++++++++++++++++++++++++++++++-------- src/monitor.rs | 15 ------------ 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9ca0498..b68485a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,7 @@ pub struct CpuDelta { impl CpuDelta { pub fn apply(&self) -> anyhow::Result<()> { - let cpus = match &self.for_ { + let mut cpus = match &self.for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); @@ -63,7 +63,7 @@ impl CpuDelta { None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, }; - for cpu in cpus { + for cpu in &mut cpus { if let Some(governor) = self.governor.as_ref() { cpu.set_governor(governor)?; } @@ -77,11 +77,11 @@ impl CpuDelta { } if let Some(mhz_minimum) = self.frequency_mhz_minimum { - cpu.set_frequency_minimum(mhz_minimum)?; + cpu.set_frequency_mhz_minimum(mhz_minimum)?; } if let Some(mhz_maximum) = self.frequency_mhz_maximum { - cpu.set_frequency_maximum(mhz_maximum)?; + cpu.set_frequency_mhz_maximum(mhz_maximum)?; } } diff --git a/src/core.rs b/src/core.rs index a3f4e33..84f1886 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,9 +6,6 @@ pub struct SystemInfo { pub struct CpuCoreInfo { // Per-core data pub core_id: u32, - pub current_frequency_mhz: Option, - pub min_frequency_mhz: Option, - pub max_frequency_mhz: Option, pub usage_percent: Option, pub temperature_celsius: Option, } diff --git a/src/cpu.rs b/src/cpu.rs index 736008a..9941020 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -11,6 +11,10 @@ pub struct Cpu { pub has_cpufreq: bool, + pub frequency_mhz: u64, + pub frequency_mhz_minimum: u64, + pub frequency_mhz_maximum: u64, + pub time_user: u64, pub time_nice: u64, pub time_system: u64, @@ -52,6 +56,10 @@ impl Cpu { number, has_cpufreq: false, + frequency_mhz: 0, + frequency_mhz_minimum: 0, + frequency_mhz_maximum: 0, + time_user: 0, time_nice: 0, time_system: 0, @@ -115,6 +123,7 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.rescan_times()?; + self.rescan_frequency()?; Ok(()) } @@ -180,6 +189,32 @@ impl Cpu { Ok(()) } + fn rescan_frequency(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + let frequency_khz = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" + )) + .with_context(|| format!("failed to find {self} frequency"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_minimum = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" + )) + .with_context(|| format!("failed to find {self} frequency minimum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + let frequency_khz_maximum = fs::read_u64(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to find {self} frequency maximum"))? + .with_context(|| format!("failed to parse {self} frequency"))?; + + self.frequency_mhz = frequency_khz / 1000; + self.frequency_mhz_minimum = frequency_khz_minimum / 1000; + self.frequency_mhz_maximum = frequency_khz_maximum / 1000; + + Ok(()) + } + pub fn get_available_governors(&self) -> Vec { let Self { number, .. } = self; @@ -308,10 +343,10 @@ impl Cpu { }) } - pub fn set_frequency_minimum(&self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = self; + pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.validate_frequency_minimum(frequency_mhz)?; + self.validate_frequency_mhz_minimum(frequency_mhz)?; // We use u64 for the intermediate calculation to prevent overflow let frequency_khz = frequency_mhz * 1000; @@ -323,10 +358,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") - }) + })?; + + self.frequency_mhz_minimum = frequency_mhz; + + Ok(()) } - fn validate_frequency_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( @@ -346,10 +385,10 @@ impl Cpu { Ok(()) } - pub fn set_frequency_maximum(&self, frequency_mhz: u64) -> anyhow::Result<()> { - let Self { number, .. } = self; + pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { + let Self { number, .. } = *self; - self.validate_frequency_maximum(frequency_mhz)?; + self.validate_frequency_mhz_maximum(frequency_mhz)?; // We use u64 for the intermediate calculation to prevent overflow let frequency_khz = frequency_mhz * 1000; @@ -361,10 +400,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") - }) + })?; + + self.frequency_mhz_maximum = frequency_mhz; + + Ok(()) } - fn validate_frequency_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { + fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( diff --git a/src/monitor.rs b/src/monitor.rs index 5d0468b..9f0c972 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -21,18 +21,6 @@ pub fn get_cpu_core_info( prev_times: &CpuTimes, current_times: &CpuTimes, ) -> 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")) - .map(|khz| khz / 1000) - .ok(); - let min_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_min_freq")) - .map(|khz| khz / 1000) - .ok(); - let max_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_max_freq")) - .map(|khz| khz / 1000) - .ok(); - // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -144,9 +132,6 @@ pub fn get_cpu_core_info( Ok(CpuCoreInfo { core_id, - current_frequency_mhz, - min_frequency_mhz, - max_frequency_mhz, usage_percent, temperature_celsius, }) From 961d1dfcd76c4db534aae65e6bd5338543e1865b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 18:27:09 +0300 Subject: [PATCH 72/93] cpu: store governor and available governors --- src/core.rs | 2 -- src/cpu.rs | 92 ++++++++++++++++++++++++++++++++------------------ src/fs.rs | 2 +- src/monitor.rs | 21 ------------ 4 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/core.rs b/src/core.rs index 84f1886..38c3d0c 100644 --- a/src/core.rs +++ b/src/core.rs @@ -12,8 +12,6 @@ pub struct CpuCoreInfo { pub struct CpuGlobalInfo { // System-wide CPU settings - pub current_governor: Option, - pub available_governors: Vec, pub turbo_status: Option, // true for enabled, false for disabled pub epp: Option, // Energy Performance Preference pub epb: Option, // Energy Performance Bias diff --git a/src/cpu.rs b/src/cpu.rs index 9941020..7365940 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -11,9 +11,12 @@ pub struct Cpu { pub has_cpufreq: bool, - pub frequency_mhz: u64, - pub frequency_mhz_minimum: u64, - pub frequency_mhz_maximum: u64, + pub available_governors: Vec, + pub governor: Option, + + pub frequency_mhz: Option, + pub frequency_mhz_minimum: Option, + pub frequency_mhz_maximum: Option, pub time_user: u64, pub time_nice: u64, @@ -56,9 +59,12 @@ impl Cpu { number, has_cpufreq: false, - frequency_mhz: 0, - frequency_mhz_minimum: 0, - frequency_mhz_maximum: 0, + available_governors: Vec::new(), + governor: None, + + frequency_mhz: None, + frequency_mhz_minimum: None, + frequency_mhz_maximum: None, time_user: 0, time_nice: 0, @@ -123,7 +129,11 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.rescan_times()?; - self.rescan_frequency()?; + + if self.has_cpufreq { + self.rescan_governor()?; + self.rescan_frequency()?; + } Ok(()) } @@ -189,6 +199,33 @@ impl Cpu { Ok(()) } + fn rescan_governor(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = *self; + + self.available_governors = 'available_governors: { + let Some(Ok(content)) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" + )) else { + break 'available_governors Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; + + self.governor = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" + )) + .with_context(|| format!("failed to find {self} scaling governor"))? + .with_context(|| format!("failed to read {self} scaling governor"))?, + ); + + Ok(()) + } + fn rescan_frequency(&mut self) -> anyhow::Result<()> { let Self { number, .. } = *self; @@ -208,32 +245,19 @@ impl Cpu { .with_context(|| format!("failed to find {self} frequency maximum"))? .with_context(|| format!("failed to parse {self} frequency"))?; - self.frequency_mhz = frequency_khz / 1000; - self.frequency_mhz_minimum = frequency_khz_minimum / 1000; - self.frequency_mhz_maximum = frequency_khz_maximum / 1000; + self.frequency_mhz = Some(frequency_khz / 1000); + self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); + self.frequency_mhz_maximum = Some(frequency_khz_maximum / 1000); Ok(()) } - pub fn get_available_governors(&self) -> Vec { - let Self { number, .. } = self; - - let Some(Ok(content)) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" - )) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - } - - pub fn set_governor(&self, governor: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let governors = self.get_available_governors(); + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { + let Self { + number, + available_governors: ref governors, + .. + } = *self; if !governors .iter() @@ -253,7 +277,11 @@ impl Cpu { format!( "this probably means that {self} doesn't exist or doesn't support changing governors" ) - }) + })?; + + self.governor = Some(governor.to_owned()); + + Ok(()) } pub fn get_available_epps(&self) -> Vec { @@ -360,7 +388,7 @@ impl Cpu { format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") })?; - self.frequency_mhz_minimum = frequency_mhz; + self.frequency_mhz_minimum = Some(frequency_mhz); Ok(()) } @@ -402,7 +430,7 @@ impl Cpu { format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") })?; - self.frequency_mhz_maximum = frequency_mhz; + self.frequency_mhz_maximum = Some(frequency_mhz); Ok(()) } diff --git a/src/fs.rs b/src/fs.rs index 9b150b3..4c11178 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -19,7 +19,7 @@ pub fn read(path: impl AsRef) -> Option> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Some(Ok(string)), + Ok(string) => Some(Ok(string.trim().to_owned())), Err(error) if error.kind() == io::ErrorKind::NotFound => None, diff --git a/src/monitor.rs b/src/monitor.rs index 9f0c972..44a7146 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -260,25 +260,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); - let current_governor = if cpufreq_base_path_buf.join("scaling_governor").exists() { - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("scaling_governor")).ok() - } else { - None - }; - - let available_governors = if cpufreq_base_path_buf - .join("scaling_available_governors") - .exists() - { - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("scaling_available_governors")) - .map_or_else( - |_| vec![], - |s| s.split_whitespace().map(String::from).collect(), - ) - } else { - vec![] - }; - let turbo_status = if turbo_status_path.exists() { // 0 means turbo enabled, 1 means disabled for intel_pstate read_sysfs_value::(turbo_status_path) @@ -325,8 +306,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - current_governor, - available_governors, turbo_status, epp: energy_perf_pref, epb: energy_perf_bias, From 99feb831a8bf0c201c0b2727198264b48196f64c Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 21 May 2025 18:43:31 +0300 Subject: [PATCH 73/93] cpu: store EPP and EPB --- src/cpu.rs | 148 +++++++++++++++++++++++++++++++------------------ src/monitor.rs | 29 ---------- 2 files changed, 95 insertions(+), 82 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 7365940..c41a0d0 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -18,6 +18,12 @@ pub struct Cpu { pub frequency_mhz_minimum: Option, pub frequency_mhz_maximum: Option, + pub available_epps: Vec, + pub epp: Option, + + pub available_epbs: Vec, + pub epb: Option, + pub time_user: u64, pub time_nice: u64, pub time_system: u64, @@ -66,6 +72,12 @@ impl Cpu { frequency_mhz_minimum: None, frequency_mhz_maximum: None, + available_epps: Vec::new(), + epp: None, + + available_epbs: Vec::new(), + epb: None, + time_user: 0, time_nice: 0, time_system: 0, @@ -133,6 +145,8 @@ impl Cpu { if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; + self.rescan_epp()?; + self.rescan_epb()?; } Ok(()) @@ -252,6 +266,75 @@ impl Cpu { Ok(()) } + fn rescan_epp(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.available_epps = 'available_epps: { + let Some(Ok(content)) = fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" + )) else { + break 'available_epps Vec::new(); + }; + + content + .split_whitespace() + .map(ToString::to_string) + .collect() + }; + + self.epp = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" + )) + .with_context(|| format!("failed to find {self} EPP"))? + .with_context(|| format!("failed to read {self} EPP"))?, + ); + + Ok(()) + } + + fn rescan_epb(&mut self) -> anyhow::Result<()> { + let Self { number, .. } = self; + + self.available_epbs = if self.has_cpufreq { + vec![ + "1".to_owned(), + "2".to_owned(), + "3".to_owned(), + "4".to_owned(), + "5".to_owned(), + "6".to_owned(), + "7".to_owned(), + "8".to_owned(), + "9".to_owned(), + "10".to_owned(), + "11".to_owned(), + "12".to_owned(), + "13".to_owned(), + "14".to_owned(), + "15".to_owned(), + "performance".to_owned(), + "balance-performance".to_owned(), + "balance_performance".to_owned(), // Alternative form with underscore. + "balance-power".to_owned(), + "balance_power".to_owned(), // Alternative form with underscore. + "power".to_owned(), + ] + } else { + Vec::new() + }; + + self.epb = Some( + fs::read(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" + )) + .with_context(|| format!("failed to find {self} EPB"))? + .with_context(|| format!("failed to read {self} EPB"))?, + ); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, @@ -284,25 +367,12 @@ impl Cpu { Ok(()) } - pub fn get_available_epps(&self) -> Vec { - let Self { number, .. } = self; - - let Some(Ok(content)) = fs::read(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" - )) else { - return Vec::new(); - }; - - content - .split_whitespace() - .map(ToString::to_string) - .collect() - } - - pub fn set_epp(&self, epp: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; - - let epps = self.get_available_epps(); + pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> { + let Self { + number, + available_epps: ref epps, + .. + } = *self; if !epps.iter().any(|avail_epp| avail_epp == epp) { bail!( @@ -320,42 +390,14 @@ impl Cpu { }) } - pub fn get_available_epbs(&self) -> &'static [&'static str] { - if !self.has_cpufreq { - return &[]; - } - - &[ - "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", - ] - } - pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { - let Self { number, .. } = self; + let Self { + number, + available_epbs: ref epbs, + .. + } = *self; - let epbs = self.get_available_epbs(); - - if !epbs.contains(&epb) { + if !epbs.iter().any(|avail_epb| avail_epb == epb) { bail!( "EPB value '{epb}' is not available for {self}. available EPB values: {valid}", valid = epbs.join(", "), diff --git a/src/monitor.rs b/src/monitor.rs index 44a7146..609213d 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -238,25 +238,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - // Find a valid CPU to read global settings from - // Try cpu0 first, then fall back to any available CPU with cpufreq - let mut cpufreq_base_path_buf = PathBuf::from("/sys/devices/system/cpu/cpu0/cpufreq/"); - - if !cpufreq_base_path_buf.exists() { - let core_count = get_real_cpus().unwrap_or_else(|e| { - eprintln!("Warning: {e}"); - 0 - }); - - for i in 0..core_count { - let test_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{i}/cpufreq/")); - if test_path.exists() { - cpufreq_base_path_buf = test_path; - break; // Exit the loop as soon as we find a valid path - } - } - } - let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); @@ -272,14 +253,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { None }; - // EPP (Energy Performance Preference) - let energy_perf_pref = - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("energy_performance_preference")).ok(); - - // EPB (Energy Performance Bias) - let energy_perf_bias = - read_sysfs_file_trimmed(cpufreq_base_path_buf.join("energy_performance_bias")).ok(); - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); // Calculate average CPU temperature from the core temperatures @@ -307,8 +280,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { turbo_status, - epp: energy_perf_pref, - epb: energy_perf_bias, platform_profile, average_temperature_celsius, } From 07ca582760712fb831520f86a1e4fb5007ec0456 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 17:42:33 +0300 Subject: [PATCH 74/93] cpu: set_ep{p,b} actually sets the attributes now --- src/cpu.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index c41a0d0..d7ab01f 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -387,10 +387,14 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPP") - }) + })?; + + self.epp = Some(epp.to_owned()); + + Ok(()) } - pub fn set_epb(&self, epb: &str) -> anyhow::Result<()> { + pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> { let Self { number, available_epbs: ref epbs, @@ -410,7 +414,11 @@ impl Cpu { ) .with_context(|| { format!("this probably means that {self} doesn't exist or doesn't support changing EPB") - }) + })?; + + self.epb = Some(epb.to_owned()); + + Ok(()) } pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { From 571f172cc2a947de62bfcb33aa4eab8b380016c4 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 17:47:21 +0300 Subject: [PATCH 75/93] cpu: add global turbo querying --- src/core.rs | 5 ++--- src/cpu.rs | 12 ++++++++++++ src/monitor.rs | 16 ---------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/core.rs b/src/core.rs index 38c3d0c..e3773c5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -12,9 +12,8 @@ pub struct CpuCoreInfo { pub struct CpuGlobalInfo { // System-wide CPU settings - pub turbo_status: Option, // true for enabled, false for disabled - pub epp: Option, // Energy Performance Preference - pub epb: Option, // Energy Performance Bias + pub epp: Option, // Energy Performance Preference + pub epb: Option, // Energy Performance Bias pub platform_profile: Option, pub average_temperature_celsius: Option, // Average temperature across all cores } diff --git a/src/cpu.rs b/src/cpu.rs index d7ab01f..3d6fd85 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -553,4 +553,16 @@ impl Cpu { bail!("no supported CPU boost control mechanism found"); } + + pub fn turbo() -> Option { + if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") { + return Some(content == 0); + } + + if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/cpufreq/boost") { + return Some(content == 1); + } + + None + } } diff --git a/src/monitor.rs b/src/monitor.rs index 609213d..4286f0b 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -238,21 +238,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - let turbo_status_path = Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo"); - let boost_path = Path::new("/sys/devices/system/cpu/cpufreq/boost"); - - let turbo_status = if turbo_status_path.exists() { - // 0 means turbo enabled, 1 means disabled for intel_pstate - read_sysfs_value::(turbo_status_path) - .map(|val| val == 0) - .ok() - } else if boost_path.exists() { - // 1 means turbo enabled, 0 means disabled for generic cpufreq boost - read_sysfs_value::(boost_path).map(|val| val == 1).ok() - } else { - None - }; - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); // Calculate average CPU temperature from the core temperatures @@ -279,7 +264,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - turbo_status, platform_profile, average_temperature_celsius, } From 4763b54c971b2705f3fe71944eaeb65a2f6543b1 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 18:01:10 +0300 Subject: [PATCH 76/93] cpu: add usage percent --- src/core.rs | 1 - src/cpu.rs | 4 ++++ src/monitor.rs | 20 -------------------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/core.rs b/src/core.rs index e3773c5..bdd7a24 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,7 +6,6 @@ pub struct SystemInfo { pub struct CpuCoreInfo { // Per-core data pub core_id: u32, - pub usage_percent: Option, pub temperature_celsius: Option, } diff --git a/src/cpu.rs b/src/cpu.rs index 3d6fd85..89e9ea9 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -49,6 +49,10 @@ impl Cpu { pub fn time_idle(&self) -> u64 { self.time_idle + self.time_iowait } + + pub fn usage(&self) -> f64 { + 1.0 - self.time_idle() as f64 / self.time_total() as f64 + } } impl fmt::Display for Cpu { diff --git a/src/monitor.rs b/src/monitor.rs index 4286f0b..7ab1bb0 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -111,28 +111,8 @@ pub fn get_cpu_core_info( } } - let usage_percent: Option = { - let prev_idle = prev_times.idle_time(); - let current_idle = current_times.idle_time(); - - let prev_total = prev_times.total_time(); - let current_total = current_times.total_time(); - - let total_diff = current_total.saturating_sub(prev_total); - let idle_diff = current_idle.saturating_sub(prev_idle); - - // Avoid division by zero if no time has passed or counters haven't changed - if total_diff == 0 { - None - } else { - let usage = 100.0 * (1.0 - (idle_diff as f32 / total_diff as f32)); - Some(usage.clamp(0.0, 100.0)) // clamp between 0 and 100 - } - }; - Ok(CpuCoreInfo { core_id, - usage_percent, temperature_celsius, }) } From 004b8796720723ab0767bca485e019855fc81cd6 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 19:49:45 +0300 Subject: [PATCH 77/93] power_supply: add more stuff and store them --- src/config.rs | 8 +-- src/core.rs | 1 - src/cpu.rs | 108 +++++++++++++++------------- src/fs.rs | 25 ++++--- src/monitor.rs | 123 -------------------------------- src/power_supply.rs | 169 ++++++++++++++++++++++++++++++++------------ src/system.rs | 10 +-- 7 files changed, 207 insertions(+), 237 deletions(-) diff --git a/src/config.rs b/src/config.rs index b68485a..4de0ac3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -119,7 +119,7 @@ pub struct PowerDelta { impl PowerDelta { pub fn apply(&self) -> anyhow::Result<()> { - let power_supplies = match &self.for_ { + let mut power_supplies = match &self.for_ { Some(names) => { let mut power_supplies = Vec::with_capacity(names.len()); @@ -136,13 +136,13 @@ impl PowerDelta { .collect(), }; - for power_supply in power_supplies { + for power_supply in &mut power_supplies { if let Some(threshold_start) = self.charge_threshold_start { - power_supply.set_charge_threshold_start(threshold_start)?; + power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?; } if let Some(threshold_end) = self.charge_threshold_end { - power_supply.set_charge_threshold_end(threshold_end)?; + power_supply.set_charge_threshold_end(threshold_end as f64 / 100.0)?; } } diff --git a/src/core.rs b/src/core.rs index bdd7a24..2e32854 100644 --- a/src/core.rs +++ b/src/core.rs @@ -13,7 +13,6 @@ pub struct CpuGlobalInfo { // System-wide CPU settings pub epp: Option, // Energy Performance Preference pub epb: Option, // Energy Performance Bias - pub platform_profile: Option, pub average_temperature_celsius: Option, // Average temperature across all cores } diff --git a/src/cpu.rs b/src/cpu.rs index 89e9ea9..6712cdf 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -160,8 +160,8 @@ impl Cpu { // TODO: Don't read this per CPU. Share the read or // find something in /sys/.../cpu{N} that does it. let content = fs::read("/proc/stat") - .context("/proc/stat does not exist")? - .context("failed to read CPU stat")?; + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; let cpu_name = format!("cpu{number}", number = self.number); @@ -175,44 +175,44 @@ impl Cpu { self.time_user = stats .next() - .with_context(|| format!("failed to find {self} user time"))? + .with_context(|| format!("failed to parse {self} user time"))? .parse() - .with_context(|| format!("failed to parse {self} user time"))?; + .with_context(|| format!("failed to find {self} user time"))?; self.time_nice = stats .next() - .with_context(|| format!("failed to find {self} nice time"))? + .with_context(|| format!("failed to parse {self} nice time"))? .parse() - .with_context(|| format!("failed to parse {self} nice time"))?; + .with_context(|| format!("failed to find {self} nice time"))?; self.time_system = stats .next() - .with_context(|| format!("failed to find {self} system time"))? + .with_context(|| format!("failed to parse {self} system time"))? .parse() - .with_context(|| format!("failed to parse {self} system time"))?; + .with_context(|| format!("failed to find {self} system time"))?; self.time_idle = stats .next() - .with_context(|| format!("failed to find {self} idle time"))? + .with_context(|| format!("failed to parse {self} idle time"))? .parse() - .with_context(|| format!("failed to parse {self} idle time"))?; + .with_context(|| format!("failed to find {self} idle time"))?; self.time_iowait = stats .next() - .with_context(|| format!("failed to find {self} iowait time"))? + .with_context(|| format!("failed to parse {self} iowait time"))? .parse() - .with_context(|| format!("failed to parse {self} iowait time"))?; + .with_context(|| format!("failed to find {self} iowait time"))?; self.time_irq = stats .next() - .with_context(|| format!("failed to find {self} irq time"))? + .with_context(|| format!("failed to parse {self} irq time"))? .parse() - .with_context(|| format!("failed to parse {self} irq time"))?; + .with_context(|| format!("failed to find {self} irq time"))?; self.time_softirq = stats .next() - .with_context(|| format!("failed to find {self} softirq time"))? + .with_context(|| format!("failed to parse {self} softirq time"))? .parse() - .with_context(|| format!("failed to parse {self} softirq time"))?; + .with_context(|| format!("failed to find {self} softirq time"))?; self.time_steal = stats .next() - .with_context(|| format!("failed to find {self} steal time"))? + .with_context(|| format!("failed to parse {self} steal time"))? .parse() - .with_context(|| format!("failed to parse {self} steal time"))?; + .with_context(|| format!("failed to find {self} steal time"))?; Ok(()) } @@ -221,9 +221,11 @@ impl Cpu { let Self { number, .. } = *self; self.available_governors = 'available_governors: { - let Some(Ok(content)) = fs::read(format!( + let Some(content) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" - )) else { + )) + .with_context(|| format!("failed to read {self} available governors"))? + else { break 'available_governors Vec::new(); }; @@ -237,8 +239,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor" )) - .with_context(|| format!("failed to find {self} scaling governor"))? - .with_context(|| format!("failed to read {self} scaling governor"))?, + .with_context(|| format!("failed to read {self} scaling governor"))? + .with_context(|| format!("failed to find {self} scaling governor"))?, ); Ok(()) @@ -247,21 +249,21 @@ impl Cpu { fn rescan_frequency(&mut self) -> anyhow::Result<()> { let Self { number, .. } = *self; - let frequency_khz = fs::read_u64(format!( + let frequency_khz = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq" )) - .with_context(|| format!("failed to find {self} frequency"))? - .with_context(|| format!("failed to parse {self} frequency"))?; - let frequency_khz_minimum = fs::read_u64(format!( + .with_context(|| format!("failed to parse {self} frequency"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_minimum = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" )) - .with_context(|| format!("failed to find {self} frequency minimum"))? - .with_context(|| format!("failed to parse {self} frequency"))?; - let frequency_khz_maximum = fs::read_u64(format!( + .with_context(|| format!("failed to parse {self} frequency minimum"))? + .with_context(|| format!("failed to find {self} frequency"))?; + let frequency_khz_maximum = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" )) - .with_context(|| format!("failed to find {self} frequency maximum"))? - .with_context(|| format!("failed to parse {self} frequency"))?; + .with_context(|| format!("failed to parse {self} frequency maximum"))? + .with_context(|| format!("failed to find {self} frequency"))?; self.frequency_mhz = Some(frequency_khz / 1000); self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000); @@ -271,12 +273,12 @@ impl Cpu { } fn rescan_epp(&mut self) -> anyhow::Result<()> { - let Self { number, .. } = self; + let Self { number, .. } = *self; self.available_epps = 'available_epps: { - let Some(Ok(content)) = fs::read(format!( + let Some(content) = fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" - )) else { + )).with_context(|| format!("failed to read {self} available EPPs"))? else { break 'available_epps Vec::new(); }; @@ -290,8 +292,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" )) - .with_context(|| format!("failed to find {self} EPP"))? - .with_context(|| format!("failed to read {self} EPP"))?, + .with_context(|| format!("failed to read {self} EPP"))? + .with_context(|| format!("failed to find {self} EPP"))?, ); Ok(()) @@ -332,8 +334,8 @@ impl Cpu { fs::read(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias" )) - .with_context(|| format!("failed to find {self} EPB"))? - .with_context(|| format!("failed to read {self} EPB"))?, + .with_context(|| format!("failed to read {self} EPB"))? + .with_context(|| format!("failed to find {self} EPB"))?, ); Ok(()) @@ -450,9 +452,11 @@ impl Cpu { fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(Ok(minimum_frequency_khz)) = fs::read_u64(format!( + let Some(minimum_frequency_khz) = fs::read_n::(format!( "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) else { + )) + .with_context(|| format!("failed to read {self} minimum frequency"))? + else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -492,9 +496,11 @@ impl Cpu { fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { let Self { number, .. } = self; - let Some(Ok(maximum_frequency_khz)) = fs::read_u64(format!( - "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq" - )) else { + let Some(maximum_frequency_khz) = fs::read_n::(format!( + "/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq" + )) + .with_context(|| format!("failed to read {self} maximum frequency"))? + else { // Just let it pass if we can't find anything. return Ok(()); }; @@ -558,15 +564,19 @@ impl Cpu { bail!("no supported CPU boost control mechanism found"); } - pub fn turbo() -> Option { - if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/intel_pstate/no_turbo") { - return Some(content == 0); + pub fn turbo() -> anyhow::Result> { + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/intel_pstate/no_turbo") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 0)); } - if let Some(Ok(content)) = fs::read_u64("/sys/devices/system/cpu/cpufreq/boost") { - return Some(content == 1); + if let Some(content) = fs::read_n::("/sys/devices/system/cpu/cpufreq/boost") + .context("failed to read CPU turbo boost status")? + { + return Ok(Some(content == 1)); } - None + Ok(None) } } diff --git a/src/fs.rs b/src/fs.rs index 4c11178..526856d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,4 +1,4 @@ -use std::{fs, io, path::Path}; +use std::{error, fs, io, path::Path, str}; use anyhow::Context; @@ -15,32 +15,35 @@ pub fn read_dir(path: impl AsRef) -> anyhow::Result { .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } -pub fn read(path: impl AsRef) -> Option> { +pub fn read(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); match fs::read_to_string(path) { - Ok(string) => Some(Ok(string.trim().to_owned())), + Ok(string) => Ok(Some(string.trim().to_owned())), - Err(error) if error.kind() == io::ErrorKind::NotFound => None, + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => Some( - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())), - ), + Err(error) => { + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) + } } } -pub fn read_u64(path: impl AsRef) -> Option> { +pub fn read_n(path: impl AsRef) -> anyhow::Result> +where + N::Err: error::Error + Send + Sync + 'static, +{ let path = path.as_ref(); match read(path)? { - Ok(content) => Some(content.trim().parse().with_context(|| { + Some(content) => Ok(Some(content.trim().parse().with_context(|| { format!( "failed to parse contents of '{path}' as a unsigned number", path = path.display(), ) - })), + })?)), - Err(error) => Some(Err(error)), + None => Ok(None), } } diff --git a/src/monitor.rs b/src/monitor.rs index 7ab1bb0..19cc69e 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -218,8 +218,6 @@ pub fn get_all_cpu_core_info() -> anyhow::Result> { } pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); - // Calculate average CPU temperature from the core temperatures let average_temperature_celsius = if cpu_cores.is_empty() { None @@ -244,120 +242,16 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { // Return the constructed CpuGlobalInfo CpuGlobalInfo { - platform_profile, average_temperature_celsius, } } pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { - let mut batteries = Vec::new(); - let power_supply_path = Path::new("/sys/class/power_supply"); - - if !power_supply_path.exists() { - return Ok(batteries); // no power supply directory - } - - let ignored_supplies = config.ignored_power_supplies.clone().unwrap_or_default(); - - // Determine overall AC connection status - let mut overall_ac_connected = false; - for entry in fs::read_dir(power_supply_path)? { - let entry = entry?; - let ps_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - // Check for AC adapter type (common names: AC, ACAD, ADP) - if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { - if ps_type == "Mains" - || ps_type == "USB_PD_DRP" - || ps_type == "USB_PD" - || ps_type == "USB_DCP" - || ps_type == "USB_CDP" - || ps_type == "USB_ACA" - { - // USB types can also provide power - if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { - if online == 1 { - overall_ac_connected = true; - break; - } - } - } - } else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { - // Fallback for type file missing - if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { - if online == 1 { - overall_ac_connected = true; - break; - } - } - } - } - // No AC adapter detected but we're on a desktop system // Default to AC power for desktops if !overall_ac_connected { overall_ac_connected = is_likely_desktop_system(); } - - for entry in fs::read_dir(power_supply_path)? { - let entry = entry?; - let ps_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - if ignored_supplies.contains(&name) { - continue; - } - - if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { - if ps_type == "Battery" { - // Skip peripheral batteries that aren't real laptop batteries - if is_peripheral_battery(&ps_path, &name) { - log::debug!("Skipping peripheral battery: {name}"); - continue; - } - - let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok(); - let capacity_percent = read_sysfs_value::(ps_path.join("capacity")).ok(); - - let power_rate_watts = if ps_path.join("power_now").exists() { - read_sysfs_value::(ps_path.join("power_now")) // uW - .map(|uw| uw as f32 / 1_000_000.0) - .ok() - } else if ps_path.join("current_now").exists() - && ps_path.join("voltage_now").exists() - { - let current_ua = read_sysfs_value::(ps_path.join("current_now")).ok(); // uA - let voltage_uv = read_sysfs_value::(ps_path.join("voltage_now")).ok(); // uV - if let (Some(c), Some(v)) = (current_ua, voltage_uv) { - // Power (W) = (Voltage (V) * Current (A)) - // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W - Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32) - } else { - None - } - } else { - None - }; - - let charge_start_threshold = - read_sysfs_value::(ps_path.join("charge_control_start_threshold")).ok(); - let charge_stop_threshold = - read_sysfs_value::(ps_path.join("charge_control_end_threshold")).ok(); - - batteries.push(BatteryInfo { - name: name.clone(), - ac_connected: overall_ac_connected, - charging_state: status_str, - capacity_percent, - power_rate_watts, - charge_start_threshold, - charge_stop_threshold, - }); - } - } - } - // If we found no batteries but have power supplies, we're likely on a desktop if batteries.is_empty() && overall_ac_connected { log::debug!("No laptop batteries found, likely a desktop system"); @@ -366,23 +260,6 @@ pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> Ok(batteries) } -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); - let batteries = get_battery_info(config)?; - let system_load = get_system_load()?; - - Ok(SystemReport { - system_info, - cpu_cores, - cpu_global, - batteries, - system_load, - timestamp: SystemTime::now(), - }) -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index 5bbcebc..f213e5b 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -44,16 +44,38 @@ const POWER_SUPPLY_THRESHOLD_CONFIGS: &[PowerSupplyThresholdConfig] = &[ ]; /// Represents a power supply that supports charge threshold control. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct PowerSupply { pub name: String, pub path: PathBuf, + pub type_: String, pub is_from_peripheral: bool, + pub charge_state: Option, + pub charge_percent: Option, + + pub charge_threshold_start: f64, + pub charge_threshold_end: f64, + + pub drain_rate_watts: Option, + pub threshold_config: Option, } +impl PowerSupply { + pub fn is_ac(&self) -> bool { + !self.is_from_peripheral + && matches!( + &*self.type_, + "Mains" | "USB_PD_DRP" | "USB_PD" | "USB_DCP" | "USB_CDP" | "USB_ACA" + ) + || self.type_.starts_with("AC") + || self.type_.contains("ACAD") + || self.type_.contains("ADP") + } +} + impl fmt::Display for PowerSupply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "power supply '{name}'", name = self.name.yellow())?; @@ -77,6 +99,15 @@ impl PowerSupply { let mut power_supply = Self { path: Path::new(POWER_SUPPLY_PATH).join(&name), name, + type_: String::new(), + + charge_state: None, + charge_percent: None, + + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, + + drain_rate_watts: None, is_from_peripheral: false, @@ -99,6 +130,15 @@ impl PowerSupply { .to_string(), path, + type_: String::new(), + + charge_state: None, + charge_percent: None, + + charge_threshold_start: 0.0, + charge_threshold_end: 1.0, + + drain_rate_watts: None, is_from_peripheral: false, @@ -131,37 +171,18 @@ impl PowerSupply { Ok(power_supplies) } - fn get_type(&self) -> anyhow::Result { - let type_path = self.path.join("type"); - - let type_ = fs::read(&type_path) - .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? - .with_context(|| format!("failed to read '{path}'", path = type_path.display()))?; - - Ok(type_) - } - pub fn rescan(&mut self) -> anyhow::Result<()> { if !self.path.exists() { bail!("{self} does not exist"); } - self.threshold_config = self - .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() - && self.path.join(config.path_end).exists() - { - return Some(*config); - } - } + self.type_ = { + let type_path = self.path.join("type"); - None - }) - .flatten(); + fs::read(&type_path) + .with_context(|| format!("failed to read '{path}'", path = type_path.display()))? + .with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? + }; self.is_from_peripheral = 'is_from_peripheral: { let name_lower = self.name.to_lowercase(); @@ -179,10 +200,9 @@ impl PowerSupply { } // Small capacity batteries are likely not laptop batteries. - if let Some(energy_full) = fs::read_u64(self.path.join("energy_full")) { - let energy_full = energy_full - .with_context(|| format!("failed to read the max charge '{self}' can hold"))?; - + if let Some(energy_full) = fs::read_n::(self.path.join("energy_full")) + .with_context(|| format!("failed to read the max charge {self} can hold"))? + { // Most laptop batteries are at least 20,000,000 µWh (20 Wh). // Peripheral batteries are typically much smaller. if energy_full < 10_000_000 { @@ -191,10 +211,9 @@ impl PowerSupply { } } // Check for model name that indicates a peripheral - if let Some(model) = fs::read(self.path.join("model_name")) { - let model = - model.with_context(|| format!("failed to read the model name of '{self}'"))?; - + if let Some(model) = fs::read(self.path.join("model_name")) + .with_context(|| format!("failed to read the model name of {self}"))? + { if model.contains("bluetooth") || model.contains("wireless") { break 'is_from_peripheral true; } @@ -203,6 +222,53 @@ impl PowerSupply { false }; + if self.type_ == "Battery" { + self.charge_state = fs::read(self.path.join("status")) + .with_context(|| format!("failed to read {self} charge status"))?; + + self.charge_percent = fs::read_n::(self.path.join("capacity")) + .with_context(|| format!("failed to read {self} charge percent"))? + .map(|percent| percent as f64 / 100.0); + + self.charge_threshold_start = + fs::read_n::(self.path.join("charge_control_start_threshold")) + .with_context(|| format!("failed to read {self} charge threshold start"))? + .map_or(0.0, |percent| percent as f64 / 100.0); + + self.charge_threshold_end = + fs::read_n::(self.path.join("charge_control_end_threshold")) + .with_context(|| format!("failed to read {self} charge threshold end"))? + .map_or(100.0, |percent| percent as f64 / 100.0); + + self.drain_rate_watts = match fs::read_n::(self.path.join("power_now")) + .with_context(|| format!("failed to read {self} power drain"))? + { + Some(drain) => Some(drain as f64), + + None => { + let current_ua = fs::read_n::(self.path.join("current_now")) + .with_context(|| format!("failed to read {self} current"))?; + + let voltage_uv = fs::read_n::(self.path.join("voltage_now")) + .with_context(|| format!("failed to read {self} voltage"))?; + + current_ua.zip(voltage_uv).map(|(current, voltage)| { + // Power (W) = Voltage (V) * Current (A) + // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W + current as f64 * voltage as f64 / 1e12 + }) + } + }; + + self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS + .iter() + .find(|config| { + self.path.join(config.path_start).exists() + && self.path.join(config.path_end).exists() + }) + .copied(); + } + Ok(()) } @@ -216,7 +282,10 @@ impl PowerSupply { .map(|config| self.path.join(config.path_end)) } - pub fn set_charge_threshold_start(&self, charge_threshold_start: u8) -> anyhow::Result<()> { + pub fn set_charge_threshold_start( + &mut self, + charge_threshold_start: f64, + ) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_start().ok_or_else(|| { anyhow!( @@ -224,16 +293,18 @@ impl PowerSupply { name = self.name, ) })?, - &charge_threshold_start.to_string(), + &((charge_threshold_start * 100.0) as u8).to_string(), ) .with_context(|| format!("failed to set charge threshold start for {self}"))?; + self.charge_threshold_start = charge_threshold_start; + log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); Ok(()) } - pub fn set_charge_threshold_end(&self, charge_threshold_end: u8) -> anyhow::Result<()> { + pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> { fs::write( &self.charge_threshold_path_end().ok_or_else(|| { anyhow!( @@ -241,26 +312,30 @@ impl PowerSupply { name = self.name, ) })?, - &charge_threshold_end.to_string(), + &((charge_threshold_end * 100.0) as u8).to_string(), ) .with_context(|| format!("failed to set charge threshold end for {self}"))?; + self.charge_threshold_end = charge_threshold_end; + log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); Ok(()) } - pub fn get_available_platform_profiles() -> Vec { + pub fn get_available_platform_profiles() -> anyhow::Result> { let path = "/sys/firmware/acpi/platform_profile_choices"; - let Some(Ok(content)) = fs::read(path) else { - return Vec::new(); + let Some(content) = + fs::read(path).context("failed to read available ACPI platform profiles")? + else { + return Ok(Vec::new()); }; - content + Ok(content .split_whitespace() .map(ToString::to_string) - .collect() + .collect()) } /// Sets the platform profile. @@ -270,7 +345,7 @@ impl PowerSupply { /// /// [`The Kernel docs`]: pub fn set_platform_profile(profile: &str) -> anyhow::Result<()> { - let profiles = Self::get_available_platform_profiles(); + let profiles = Self::get_available_platform_profiles()?; if !profiles .iter() @@ -285,4 +360,10 @@ impl PowerSupply { fs::write("/sys/firmware/acpi/platform_profile", profile) .context("this probably means that your system does not support changing ACPI profiles") } + + pub fn platform_profile() -> anyhow::Result { + fs::read("/sys/firmware/acpi/platform_profile") + .context("failed to read platform profile")? + .context("failed to find platform profile") + } } diff --git a/src/system.rs b/src/system.rs index f8820b1..5781c3c 100644 --- a/src/system.rs +++ b/src/system.rs @@ -33,9 +33,9 @@ impl System { } fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { - if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type") { - let chassis_type = chassis_type.context("failed to read chassis type")?; - + if let Some(chassis_type) = + fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? + { // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis @@ -86,8 +86,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("load average file doesn't exist, are you on linux?")? - .context("failed to read load average")?; + .context("failed to read load average")? + .context("load average file doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace(); From a343e38d9539ebb073a77eaad615fb62b4b88d3b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 22 May 2025 20:12:25 +0300 Subject: [PATCH 78/93] system: is_ac --- src/monitor.rs | 14 -------------- src/system.rs | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/monitor.rs b/src/monitor.rs index 19cc69e..88195c8 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -246,20 +246,6 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { } } -pub fn get_battery_info(config: &AppConfig) -> anyhow::Result> { - // No AC adapter detected but we're on a desktop system - // Default to AC power for desktops - if !overall_ac_connected { - overall_ac_connected = is_likely_desktop_system(); - } - // If we found no batteries but have power supplies, we're likely on a desktop - if batteries.is_empty() && overall_ac_connected { - log::debug!("No laptop batteries found, likely a desktop system"); - } - - Ok(batteries) -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/system.rs b/src/system.rs index 5781c3c..17f8b73 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,19 +1,25 @@ use anyhow::{Context, bail}; -use crate::fs; +use crate::{cpu, fs, power_supply}; pub struct System { - pub is_desktop: bool, + pub is_ac: bool, pub load_average_1min: f64, pub load_average_5min: f64, pub load_average_15min: f64, + + pub cpus: Vec, + pub power_supplies: Vec, } impl System { pub fn new() -> anyhow::Result { let mut system = Self { - is_desktop: false, + is_ac: false, + + cpus: Vec::new(), + power_supplies: Vec::new(), load_average_1min: 0.0, load_average_5min: 0.0, @@ -26,13 +32,23 @@ impl System { } pub fn rescan(&mut self) -> anyhow::Result<()> { - self.rescan_is_desktop()?; + self.cpus = cpu::Cpu::all().context("failed to scan CPUs")?; + + self.power_supplies = + power_supply::PowerSupply::all().context("failed to scan power supplies")?; + + self.is_ac = self + .power_supplies + .iter() + .any(|power_supply| power_supply.is_ac()) + || self.is_desktop()?; + self.rescan_load_average()?; Ok(()) } - fn rescan_is_desktop(&mut self) -> anyhow::Result<()> { + fn is_desktop(&mut self) -> anyhow::Result { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? { @@ -42,13 +58,11 @@ impl System { match chassis_type.trim() { // Desktop form factors. "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { - self.is_desktop = true; - return Ok(()); + return Ok(true); } // Laptop form factors. "9" | "10" | "14" => { - self.is_desktop = false; - return Ok(()); + return Ok(false); } // Unknown, continue with other checks @@ -61,8 +75,7 @@ impl System { || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); if !power_saving_exists { - self.is_desktop = true; - return Ok(()); // Likely a desktop. + return Ok(true); // Likely a desktop. } // Check battery-specific ACPI paths that laptops typically have @@ -74,14 +87,12 @@ impl System { for path in laptop_acpi_paths { if fs::exists(path) { - self.is_desktop = false; // Likely a laptop. - return Ok(()); + return Ok(false); // Likely a laptop. } } // Default to assuming desktop if we can't determine. - self.is_desktop = true; - Ok(()) + Ok(true) } fn rescan_load_average(&mut self) -> anyhow::Result<()> { From 2812baa77beae77a6b2fd2f526e798d00a84e41e Mon Sep 17 00:00:00 2001 From: RGBCube Date: Tue, 27 May 2025 21:24:06 +0300 Subject: [PATCH 79/93] system: check for chassis type 31 and move power saving check below Co-Authored-By: flashrun24 --- src/system.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/system.rs b/src/system.rs index 17f8b73..02b7b80 100644 --- a/src/system.rs +++ b/src/system.rs @@ -52,16 +52,18 @@ impl System { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? { - // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower - // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One - // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis + // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower, + // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One, + // 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis, + // 31=Convertible Laptop match chassis_type.trim() { // Desktop form factors. "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { return Ok(true); } + // Laptop form factors. - "9" | "10" | "14" => { + "9" | "10" | "14" | "31" => { return Ok(false); } @@ -70,14 +72,6 @@ impl System { } } - // Check CPU power policies, desktops often don't have these - let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") - || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); - - if !power_saving_exists { - return Ok(true); // Likely a desktop. - } - // Check battery-specific ACPI paths that laptops typically have let laptop_acpi_paths = [ "/sys/class/power_supply/BAT0", @@ -91,6 +85,14 @@ impl System { } } + // Check CPU power policies, desktops often don't have these + let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") + || fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); + + if !power_saving_exists { + return Ok(true); // Likely a desktop. + } + // Default to assuming desktop if we can't determine. Ok(true) } From fd3ae29dc5592926d77ffdda7247371f89e04795 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 28 May 2025 21:47:44 +0300 Subject: [PATCH 80/93] cpu: cache /proc/stat --- src/config.rs | 3 +- src/cpu.rs | 169 +++++++++++++++++++++++++------------------------- 2 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/config.rs b/src/config.rs index 4de0ac3..2560806 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,9 +53,10 @@ impl CpuDelta { let mut cpus = match &self.for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); + let cache = cpu::CpuRescanCache::default(); for &number in numbers { - cpus.push(cpu::Cpu::new(number)?); + cpus.push(cpu::Cpu::new(number, &cache)?); } cpus diff --git a/src/cpu.rs b/src/cpu.rs index 6712cdf..4979b67 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,10 +1,27 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{fmt, string::ToString}; +use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; use crate::fs; +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct CpuRescanCache { + stat: OnceCell>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CpuStat { + pub user: u64, + pub nice: u64, + pub system: u64, + pub idle: u64, + pub iowait: u64, + pub irq: u64, + pub softirq: u64, + pub steal: u64, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cpu { pub number: u32, @@ -24,30 +41,23 @@ pub struct Cpu { pub available_epbs: Vec, pub epb: Option, - pub time_user: u64, - pub time_nice: u64, - pub time_system: u64, - pub time_idle: u64, - pub time_iowait: u64, - pub time_irq: u64, - pub time_softirq: u64, - pub time_steal: u64, + pub stat: CpuStat, } impl Cpu { pub fn time_total(&self) -> u64 { - self.time_user - + self.time_nice - + self.time_system - + self.time_idle - + self.time_iowait - + self.time_irq - + self.time_softirq - + self.time_steal + self.stat.user + + self.stat.nice + + self.stat.system + + self.stat.idle + + self.stat.iowait + + self.stat.irq + + self.stat.softirq + + self.stat.steal } pub fn time_idle(&self) -> u64 { - self.time_idle + self.time_iowait + self.stat.idle + self.stat.iowait } pub fn usage(&self) -> f64 { @@ -64,7 +74,7 @@ impl fmt::Display for Cpu { } impl Cpu { - pub fn new(number: u32) -> anyhow::Result { + pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result { let mut cpu = Self { number, has_cpufreq: false, @@ -82,16 +92,18 @@ impl Cpu { available_epbs: Vec::new(), epb: None, - time_user: 0, - time_nice: 0, - time_system: 0, - time_idle: 0, - time_iowait: 0, - time_irq: 0, - time_softirq: 0, - time_steal: 0, + stat: CpuStat { + user: 0, + nice: 0, + system: 0, + idle: 0, + iowait: 0, + irq: 0, + softirq: 0, + steal: 0, + }, }; - cpu.rescan()?; + cpu.rescan(cache)?; Ok(cpu) } @@ -101,6 +113,7 @@ impl Cpu { const PATH: &str = "/sys/devices/system/cpu"; let mut cpus = vec![]; + let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) .with_context(|| format!("failed to read contents of '{PATH}'"))? @@ -121,13 +134,13 @@ impl Cpu { continue; }; - cpus.push(Self::new(number)?); + cpus.push(Self::new(number, &cache)?); } // 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)?); + cpus.push(Self::new(number, &cache)?); } } @@ -135,7 +148,7 @@ impl Cpu { } /// Rescan CPU, tuning local copy of settings. - pub fn rescan(&mut self) -> anyhow::Result<()> { + pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { let Self { number, .. } = self; if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { @@ -144,7 +157,7 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.rescan_times()?; + self.rescan_stat(cache)?; if self.has_cpufreq { self.rescan_governor()?; @@ -156,63 +169,47 @@ impl Cpu { Ok(()) } - fn rescan_times(&mut self) -> anyhow::Result<()> { - // TODO: Don't read this per CPU. Share the read or - // find something in /sys/.../cpu{N} that does it. - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, - let cpu_name = format!("cpu{number}", number = self.number); + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; - let mut stats = content - .lines() - .find_map(|line| { - line.starts_with(&cpu_name) - .then(|| line.split_whitespace().skip(1)) - }) - .with_context(|| format!("failed to find {self} in CPU stats"))?; + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - self.time_user = stats - .next() - .with_context(|| format!("failed to parse {self} user time"))? - .parse() - .with_context(|| format!("failed to find {self} user time"))?; - self.time_nice = stats - .next() - .with_context(|| format!("failed to parse {self} nice time"))? - .parse() - .with_context(|| format!("failed to find {self} nice time"))?; - self.time_system = stats - .next() - .with_context(|| format!("failed to parse {self} system time"))? - .parse() - .with_context(|| format!("failed to find {self} system time"))?; - self.time_idle = stats - .next() - .with_context(|| format!("failed to parse {self} idle time"))? - .parse() - .with_context(|| format!("failed to find {self} idle time"))?; - self.time_iowait = stats - .next() - .with_context(|| format!("failed to parse {self} iowait time"))? - .parse() - .with_context(|| format!("failed to find {self} iowait time"))?; - self.time_irq = stats - .next() - .with_context(|| format!("failed to parse {self} irq time"))? - .parse() - .with_context(|| format!("failed to find {self} irq time"))?; - self.time_softirq = stats - .next() - .with_context(|| format!("failed to parse {self} softirq time"))? - .parse() - .with_context(|| format!("failed to find {self} softirq time"))?; - self.time_steal = stats - .next() - .with_context(|| format!("failed to parse {self} steal time"))? - .parse() - .with_context(|| format!("failed to find {self} steal time"))?; + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); Ok(()) } From 421d4aaacc28ee0939f65104736eb39dfa8bc3c3 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 28 May 2025 22:35:31 +0300 Subject: [PATCH 81/93] cpu: wip temperature scanning, waiting for raf to stand up from his desk and open his laptop on the other side of the room --- src/cpu.rs | 203 +++++++++++++++++++++++++++++--------------- src/fs.rs | 18 ++-- src/monitor.rs | 65 +------------- src/power_supply.rs | 1 + src/system.rs | 4 +- 5 files changed, 149 insertions(+), 142 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 4979b67..3c0f332 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,9 +5,10 @@ use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; use crate::fs; -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, + temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -22,7 +23,28 @@ pub struct CpuStat { pub steal: u64, } -#[derive(Debug, Clone, PartialEq, Eq)] +impl CpuStat { + pub fn total(&self) -> u64 { + self.user + + self.nice + + self.system + + self.idle + + self.iowait + + self.irq + + self.softirq + + self.steal + } + + pub fn idle(&self) -> u64 { + self.idle + self.iowait + } + + pub fn usage(&self) -> f64 { + 1.0 - self.idle() as f64 / self.total() as f64 + } +} + +#[derive(Debug, Clone, PartialEq)] pub struct Cpu { pub number: u32, @@ -42,27 +64,8 @@ pub struct Cpu { pub epb: Option, pub stat: CpuStat, -} -impl Cpu { - pub fn time_total(&self) -> u64 { - self.stat.user - + self.stat.nice - + self.stat.system - + self.stat.idle - + self.stat.iowait - + self.stat.irq - + self.stat.softirq - + self.stat.steal - } - - pub fn time_idle(&self) -> u64 { - self.stat.idle + self.stat.iowait - } - - pub fn usage(&self) -> f64 { - 1.0 - self.time_idle() as f64 / self.time_total() as f64 - } + pub temperature: Option, } impl fmt::Display for Cpu { @@ -102,6 +105,8 @@ impl Cpu { softirq: 0, steal: 0, }, + + temperature: None, }; cpu.rescan(cache)?; @@ -116,9 +121,11 @@ impl Cpu { let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read contents of '{PATH}'"))? - .flatten() + .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + let entry_file_name = entry.file_name(); let Some(name) = entry_file_name.to_str() else { @@ -157,8 +164,6 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); - self.rescan_stat(cache)?; - if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; @@ -166,50 +171,8 @@ impl Cpu { self.rescan_epb()?; } - Ok(()) - } - - fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let stat = match cache.stat.get() { - Some(stat) => stat, - - None => { - let content = fs::read("/proc/stat") - .context("failed to read CPU stat")? - .context("/proc/stat does not exist")?; - - cache - .stat - .set(HashMap::from_iter(content.lines().skip(1).filter_map( - |line| { - let mut parts = line.strip_prefix("cpu")?.split_whitespace(); - - let number = parts.next()?.parse().ok()?; - - let stat = CpuStat { - user: parts.next()?.parse().ok()?, - nice: parts.next()?.parse().ok()?, - system: parts.next()?.parse().ok()?, - idle: parts.next()?.parse().ok()?, - iowait: parts.next()?.parse().ok()?, - irq: parts.next()?.parse().ok()?, - softirq: parts.next()?.parse().ok()?, - steal: parts.next()?.parse().ok()?, - }; - - Some((number, stat)) - }, - ))); - - cache.stat.get().unwrap() - } - }; - - self.stat = stat - .get(&self.number) - .with_context(|| format!("failed to get stat of {self}"))? - .clone(); + self.rescan_stat(cache)?; + self.rescan_temperature(cache)?; Ok(()) } @@ -338,6 +301,106 @@ impl Cpu { Ok(()) } + fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let stat = match cache.stat.get() { + Some(stat) => stat, + + None => { + let content = fs::read("/proc/stat") + .context("failed to read CPU stat")? + .context("/proc/stat does not exist")?; + + cache + .stat + .set(HashMap::from_iter(content.lines().skip(1).filter_map( + |line| { + let mut parts = line.strip_prefix("cpu")?.split_whitespace(); + + let number = parts.next()?.parse().ok()?; + + let stat = CpuStat { + user: parts.next()?.parse().ok()?, + nice: parts.next()?.parse().ok()?, + system: parts.next()?.parse().ok()?, + idle: parts.next()?.parse().ok()?, + iowait: parts.next()?.parse().ok()?, + irq: parts.next()?.parse().ok()?, + softirq: parts.next()?.parse().ok()?, + steal: parts.next()?.parse().ok()?, + }; + + Some((number, stat)) + }, + ))) + .unwrap(); + + cache.stat.get().unwrap() + } + }; + + self.stat = stat + .get(&self.number) + .with_context(|| format!("failed to get stat of {self}"))? + .clone(); + + Ok(()) + } + + fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let temperatures = match cache.temperatures.get() { + Some(temperature) => temperature, + + None => { + const PATH: &str = "/sys/class/hwmon"; + + let temperatures = HashMap::new(); + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = + entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; + + match &*name { + // Intel CPU temperature driver + "coretemp" => todo!(), + + // AMD CPU temperature driver + // TODO: 'zenergy' can also report those stats, I think? + "k10temp" | "zenpower" | "amdgpu" => todo!(), + + // Other CPU temperature drivers + _ if name.contains("cpu") || name.contains("temp") => todo!(), + + _ => {} + } + } + + cache.temperatures.set(temperatures).unwrap(); + cache.temperatures.get().unwrap() + } + }; + + self.temperature = temperatures.get(&self.number).copied(); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/fs.rs b/src/fs.rs index 526856d..3192e4d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -8,11 +8,19 @@ pub fn exists(path: impl AsRef) -> bool { path.exists() } -pub fn read_dir(path: impl AsRef) -> anyhow::Result { +pub fn read_dir(path: impl AsRef) -> anyhow::Result> { let path = path.as_ref(); - fs::read_dir(path) - .with_context(|| format!("failed to read directory '{path}'", path = path.display())) + match fs::read_dir(path) { + Ok(entries) => Ok(Some(entries)), + + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), + + Err(error) => Err(error).context(format!( + "failed to read directory '{path}'", + path = path.display() + )), + } } pub fn read(path: impl AsRef) -> anyhow::Result> { @@ -23,9 +31,7 @@ pub fn read(path: impl AsRef) -> anyhow::Result> { Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => { - Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) - } + Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), } } diff --git a/src/monitor.rs b/src/monitor.rs index 88195c8..d4534ba 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -16,11 +16,7 @@ pub fn get_system_info() -> SystemInfo { SystemInfo { cpu_model } } -pub fn get_cpu_core_info( - core_id: u32, - prev_times: &CpuTimes, - current_times: &CpuTimes, -) -> anyhow::Result { +pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -187,65 +183,6 @@ fn get_fallback_temperature(hw_path: &Path) -> Option { None } -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()?; - - 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); - - for core_id in 0..num_cores { - if let (Some(prev), Some(curr)) = ( - initial_cpu_times.get(&core_id), - final_cpu_times.get(&core_id), - ) { - match get_cpu_core_info(core_id, prev, curr) { - Ok(info) => core_infos.push(info), - Err(e) => { - // Log or handle error for a single core, maybe push a partial info or skip - eprintln!("Error getting info for core {core_id}: {e}"); - } - } - } else { - // Log or handle missing times for a core - eprintln!("Missing CPU time data for core {core_id}"); - } - } - Ok(core_infos) -} - -pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { - // Calculate average CPU temperature from the core temperatures - let average_temperature_celsius = if cpu_cores.is_empty() { - None - } else { - // Filter cores with temperature readings, then calculate average - let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores - .iter() - .filter(|core| core.temperature_celsius.is_some()) - .collect(); - - if cores_with_temp.is_empty() { - None - } else { - // Sum up all temperatures and divide by count - let sum: f32 = cores_with_temp - .iter() - .map(|core| core.temperature_celsius.unwrap()) - .sum(); - Some(sum / cores_with_temp.len() as f32) - } - }; - - // Return the constructed CpuGlobalInfo - CpuGlobalInfo { - average_temperature_celsius, - } -} - pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index f213e5b..2155c29 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -155,6 +155,7 @@ impl PowerSupply { for entry in fs::read_dir(POWER_SUPPLY_PATH) .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? + .with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? { let entry = match entry { Ok(entry) => entry, diff --git a/src/system.rs b/src/system.rs index 02b7b80..57d5ce2 100644 --- a/src/system.rs +++ b/src/system.rs @@ -99,8 +99,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("failed to read load average")? - .context("load average file doesn't exist, are you on linux?")?; + .context("failed to read load average from '/proc/loadavg'")? + .context("'/proc/loadavg' doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace(); From 303a84479c0f2b117d8f88c1598b574328b23e45 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 18:34:40 +0300 Subject: [PATCH 82/93] system: cpu temperatures scanning --- src/cpu.rs | 56 -------------- src/monitor.rs | 194 ++++++------------------------------------------- src/system.rs | 110 ++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 227 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 3c0f332..7a60ee3 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -8,7 +8,6 @@ use crate::fs; #[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, - temperatures: OnceCell>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -172,7 +171,6 @@ impl Cpu { } self.rescan_stat(cache)?; - self.rescan_temperature(cache)?; Ok(()) } @@ -347,60 +345,6 @@ impl Cpu { Ok(()) } - fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { - // OnceCell::get_or_try_init is unstable. Cope: - let temperatures = match cache.temperatures.get() { - Some(temperature) => temperature, - - None => { - const PATH: &str = "/sys/class/hwmon"; - - let temperatures = HashMap::new(); - - for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read hardware information from '{PATH}'"))? - .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? - { - let entry = - entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - - let entry_path = entry.path(); - - let Some(name) = fs::read(entry_path.join("name")).with_context(|| { - format!( - "failed to read name of hardware entry at '{path}'", - path = entry_path.display(), - ) - })? - else { - continue; - }; - - match &*name { - // Intel CPU temperature driver - "coretemp" => todo!(), - - // AMD CPU temperature driver - // TODO: 'zenergy' can also report those stats, I think? - "k10temp" | "zenpower" | "amdgpu" => todo!(), - - // Other CPU temperature drivers - _ if name.contains("cpu") || name.contains("temp") => todo!(), - - _ => {} - } - } - - cache.temperatures.set(temperatures).unwrap(); - cache.temperatures.get().unwrap() - } - }; - - self.temperature = temperatures.get(&self.number).copied(); - - Ok(()) - } - pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/monitor.rs b/src/monitor.rs index d4534ba..40b0242 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -10,178 +10,30 @@ use std::{ time::SystemTime, }; -pub fn get_system_info() -> SystemInfo { - let cpu_model = get_cpu_model().unwrap_or_else(|_| "Unknown".to_string()); +// Try /sys/devices/platform paths for thermal zones as a last resort +// if temperature_celsius.is_none() { +// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { +// for entry in thermal_zones.flatten() { +// let zone_path = entry.path(); +// let name = entry.file_name().into_string().unwrap_or_default(); - SystemInfo { cpu_model } -} - -pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { - // Temperature detection. - // Should be generic enough to be able to support for multiple hardware sensors - // with the possibility of extending later down the road. - let mut temperature_celsius: Option = None; - - // Search for temperature in hwmon devices - if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") { - for hw_entry in hwmon_dir.flatten() { - let hw_path = hw_entry.path(); - - // Check hwmon driver name - if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) { - // Intel CPU temperature driver - if name == "coretemp" { - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - } - // AMD CPU temperature driver - // TODO: 'zenergy' can also report those stats, I think? - else if name == "k10temp" || name == "zenpower" || name == "amdgpu" { - // AMD's k10temp doesn't always label cores individually - // First try to find core-specific temps - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Tdie") { - temperature_celsius = Some(temp); - break; - } - - // Try Tctl temperature (CPU control temp) - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "Tctl") { - temperature_celsius = Some(temp); - break; - } - - // Try CPU temperature - if let Some(temp) = get_generic_sensor_temperature(&hw_path, "CPU") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any available temperature input without a specific label - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - // Other CPU temperature drivers - else if name.contains("cpu") || name.contains("temp") { - // Try to find a label that matches this core - if let Some(temp) = get_temperature_for_core(&hw_path, core_id, "Core") { - temperature_celsius = Some(temp); - break; - } - - // Fall back to any temperature reading if specific core not found - temperature_celsius = get_fallback_temperature(&hw_path); - if temperature_celsius.is_some() { - break; - } - } - } - } - } - - // Try /sys/devices/platform paths for thermal zones as a last resort - if temperature_celsius.is_none() { - if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { - for entry in thermal_zones.flatten() { - let zone_path = entry.path(); - let name = entry.file_name().into_string().unwrap_or_default(); - - if name.starts_with("thermal_zone") { - // Try to match by type - if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { - if zone_type.contains("cpu") - || zone_type.contains("x86") - || zone_type.contains("core") - { - if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { - temperature_celsius = Some(temp_mc as f32 / 1000.0); - break; - } - } - } - } - } - } - } - - Ok(CpuCoreInfo { - core_id, - temperature_celsius, - }) -} - -/// Finds core-specific temperature -fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option { - for i in 1..=32 { - // Increased range to handle systems with many sensors - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - // Match various common label formats: - // "Core X", "core X", "Core-X", "CPU Core X", etc. - let core_pattern = format!("{label_prefix} {core_id}"); - let alt_pattern = format!("{label_prefix}-{core_id}"); - - if label.eq_ignore_ascii_case(&core_pattern) - || label.eq_ignore_ascii_case(&alt_pattern) - || label - .to_lowercase() - .contains(&format!("core {core_id}").to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } - } - None -} - -// Finds generic sensor temperatures by label -fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option { - for i in 1..=32 { - let label_path = hw_path.join(format!("temp{i}_label")); - let input_path = hw_path.join(format!("temp{i}_input")); - - if label_path.exists() && input_path.exists() { - if let Ok(label) = read_sysfs_file_trimmed(&label_path) { - if label.eq_ignore_ascii_case(label_name) - || label.to_lowercase().contains(&label_name.to_lowercase()) - { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - } else if !label_path.exists() && input_path.exists() { - // Some sensors might not have labels but still have valid temp inputs - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} - -// Fallback to any temperature reading from a sensor -fn get_fallback_temperature(hw_path: &Path) -> Option { - for i in 1..=32 { - let input_path = hw_path.join(format!("temp{i}_input")); - - if input_path.exists() { - if let Ok(temp_mc) = read_sysfs_value::(&input_path) { - return Some(temp_mc as f32 / 1000.0); - } - } - } - None -} +// if name.starts_with("thermal_zone") { +// // Try to match by type +// if let Ok(zone_type) = read_sysfs_file_trimmed(zone_path.join("type")) { +// if zone_type.contains("cpu") +// || zone_type.contains("x86") +// || zone_type.contains("core") +// { +// if let Ok(temp_mc) = read_sysfs_value::(zone_path.join("temp")) { +// temperature_celsius = Some(temp_mc as f32 / 1000.0); +// break; +// } +// } +// } +// } +// } +// } +// } pub fn get_cpu_model() -> anyhow::Result { let path = Path::new("/proc/cpuinfo"); diff --git a/src/system.rs b/src/system.rs index 57d5ce2..4a86893 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, path::Path}; + use anyhow::{Context, bail}; use crate::{cpu, fs, power_supply}; @@ -10,6 +12,8 @@ pub struct System { pub load_average_15min: f64, pub cpus: Vec, + pub cpu_temperatures: HashMap, + pub power_supplies: Vec, } @@ -19,6 +23,8 @@ impl System { is_ac: false, cpus: Vec::new(), + cpu_temperatures: HashMap::new(), + power_supplies: Vec::new(), load_average_1min: 0.0, @@ -48,6 +54,110 @@ impl System { Ok(()) } + fn rescan_temperatures(&mut self) -> anyhow::Result<()> { + const PATH: &str = "/sys/class/hwmon"; + + let mut temperatures = HashMap::new(); + + for entry in fs::read_dir(PATH) + .with_context(|| format!("failed to read hardware information from '{PATH}'"))? + .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + { + let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; + + let entry_path = entry.path(); + + let Some(name) = fs::read(entry_path.join("name")).with_context(|| { + format!( + "failed to read name of hardware entry at '{path}'", + path = entry_path.display(), + ) + })? + else { + continue; + }; + + match &*name { + // TODO: 'zenergy' can also report those stats, I think? + "coretemp" | "k10temp" | "zenpower" | "amdgpu" => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + // Other CPU temperature drivers. + _ if name.contains("cpu") || name.contains("temp") => { + Self::get_temperatures(&entry_path, &mut temperatures)?; + } + + _ => {} + } + } + + self.cpu_temperatures = temperatures; + + Ok(()) + } + + fn get_temperatures( + device_path: &Path, + temperatures: &mut HashMap, + ) -> anyhow::Result<()> { + // Increased range to handle systems with many sensors. + for i in 1..=96 { + let label_path = device_path.join(format!("temp{i}_label")); + let input_path = device_path.join(format!("temp{i}_input")); + + if !label_path.exists() || !input_path.exists() { + continue; + } + + let Some(label) = fs::read(&label_path).with_context(|| { + format!( + "failed to read hardware hardware device label from '{path}'", + path = label_path.display(), + ) + })? + else { + continue; + }; + + // Match various common label formats: + // "Core X", "core X", "Core-X", "CPU Core X", etc. + let number = label + .trim_start_matches("cpu") + .trim_start_matches("CPU") + .trim_start() + .trim_start_matches("core") + .trim_start_matches("Core") + .trim_start() + .trim_start_matches("tdie") + .trim_start_matches("Tdie") + .trim_start() + .trim_start_matches("tctl") + .trim_start_matches("Tctl") + .trim_start() + .trim_start_matches("-") + .trim(); + + let Ok(number) = number.parse::() else { + continue; + }; + + let Some(temperature_mc) = fs::read_n::(&input_path).with_context(|| { + format!( + "failed to read CPU temperature from '{path}'", + path = input_path.display(), + ) + })? + else { + continue; + }; + + temperatures.insert(number, temperature_mc as f64 / 1000.0); + } + + Ok(()) + } + fn is_desktop(&mut self) -> anyhow::Result { if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? From ee7ea6b86dd46a616f58aa2e5cfc926f7d7d872e Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 18:51:27 +0300 Subject: [PATCH 83/93] cpu: add cpu info scanning --- src/cpu.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- src/monitor.rs | 31 --------------------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 7a60ee3..70918be 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,13 +1,14 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; +use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString}; use crate::fs; #[derive(Default, Debug, Clone, PartialEq)] pub struct CpuRescanCache { stat: OnceCell>, + info: OnceCell>>>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -63,6 +64,7 @@ pub struct Cpu { pub epb: Option, pub stat: CpuStat, + pub info: Option>>, pub temperature: Option, } @@ -104,6 +106,7 @@ impl Cpu { softirq: 0, steal: 0, }, + info: None, temperature: None, }; @@ -171,6 +174,7 @@ impl Cpu { } self.rescan_stat(cache)?; + self.rescan_info(cache)?; Ok(()) } @@ -345,6 +349,57 @@ impl Cpu { Ok(()) } + fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + // OnceCell::get_or_try_init is unstable. Cope: + let info = match cache.info.get() { + Some(stat) => stat, + + None => { + let content = fs::read("/proc/cpuinfo") + .context("failed to read CPU info")? + .context("/proc/cpuinfo does not exist")?; + + let mut info = HashMap::new(); + let mut current_number = None; + let mut current_data = HashMap::new(); + + macro_rules! try_save_data { + () => { + if let Some(number) = current_number.take() { + info.insert(number, Rc::new(mem::take(&mut current_data))); + } + }; + } + + for line in content.lines() { + let parts = line.splitn(2, ':').collect::>(); + + if parts.len() == 2 { + let key = parts[0].trim(); + let value = parts[1].trim(); + + if key == "processor" { + try_save_data!(); + + current_number = value.parse::().ok(); + } else { + current_data.insert(key.to_owned(), value.to_owned()); + } + } + } + + try_save_data!(); + + cache.info.set(info).unwrap(); + cache.info.get().unwrap() + } + }; + + self.info = info.get(&self.number).cloned(); + + Ok(()) + } + pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { let Self { number, diff --git a/src/monitor.rs b/src/monitor.rs index 40b0242..e4ff659 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,15 +1,3 @@ -use crate::config::AppConfig; -use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport}; -use std::{ - collections::HashMap, - fs, - path::{Path, PathBuf}, - str::FromStr, - thread, - time::Duration, - time::SystemTime, -}; - // Try /sys/devices/platform paths for thermal zones as a last resort // if temperature_celsius.is_none() { // if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") { @@ -34,22 +22,3 @@ use std::{ // } // } // } - -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())) - })?; - - for line in content.lines() { - if line.starts_with("model name") { - if let Some(val) = line.split(':').nth(1) { - let cpu_model = val.trim().to_string(); - return Ok(cpu_model); - } - } - } - Err(SysMonitorError::ParseError( - "Could not find CPU model name in /proc/cpuinfo.".to_string(), - )) -} From 1283e5be14694d2ee661f9f360b24e419deee63f Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 18:59:04 +0300 Subject: [PATCH 84/93] delete core.rs --- src/core.rs | 51 --------------------------------------------------- src/daemon.rs | 4 +++- 2 files changed, 3 insertions(+), 52 deletions(-) delete mode 100644 src/core.rs diff --git a/src/core.rs b/src/core.rs deleted file mode 100644 index 2e32854..0000000 --- a/src/core.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub struct SystemInfo { - // Overall system details - pub cpu_model: String, -} - -pub struct CpuCoreInfo { - // Per-core data - pub core_id: u32, - pub temperature_celsius: Option, -} - -pub struct CpuGlobalInfo { - // System-wide CPU settings - pub epp: Option, // Energy Performance Preference - pub epb: Option, // Energy Performance Bias - pub average_temperature_celsius: Option, // Average temperature across all cores -} - -pub struct BatteryInfo { - // Battery status (AC connected, charging state, capacity, power rate, charge start/stop thresholds if available). - pub name: String, - pub ac_connected: bool, - pub charging_state: Option, // e.g., "Charging", "Discharging", "Full" - pub capacity_percent: Option, - pub power_rate_watts: Option, // positive for charging, negative for discharging - pub charge_start_threshold: Option, - pub charge_stop_threshold: Option, -} - -pub struct SystemLoad { - // System load averages. - pub load_avg_1min: f32, - pub load_avg_5min: f32, - pub load_avg_15min: f32, -} - -pub struct SystemReport { - // Now combine all the above for a snapshot of the system state. - pub system_info: SystemInfo, - pub cpu_cores: Vec, - pub cpu_global: CpuGlobalInfo, - pub batteries: Vec, - pub system_load: SystemLoad, - pub timestamp: std::time::SystemTime, // so we know when the report was generated -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationalMode { - Powersave, - Performance, -} diff --git a/src/daemon.rs b/src/daemon.rs index f2d2e3a..f5bbbdc 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -10,7 +10,7 @@ use std::{ use anyhow::Context; -use crate::config; +use crate::{config, system}; /// Calculate the idle time multiplier based on system idle time. /// @@ -254,6 +254,8 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { }) .context("failed to set Ctrl-C handler")?; + let mut system = system::System::new()?; + while !cancelled.load(Ordering::SeqCst) {} log::info!("exiting..."); From c2325fa5ed765b765f59b17d4fa87b0d5e3edbf1 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 19:09:23 +0300 Subject: [PATCH 85/93] daemon: set charging --- src/daemon.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index f5bbbdc..82c927b 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -139,7 +139,7 @@ impl Daemon { /// The discharge rate is averaged per hour. /// So a return value of Some(0.3) means the battery has been /// discharging 30% per hour. - fn power_supply_discharge_rate(&self) -> Option { + fn power_supply_discharge_rate(&mut self) -> Option { let mut last_charge = None; // A list of increasing charge percentages. @@ -159,7 +159,9 @@ impl Daemon { }) .collect(); - if discharging.len() < 2 { + self.charging = discharging.len() < 2; + + if self.charging { return None; } From 7503e235a32093b00672d499d63b425511e3786b Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 19:13:39 +0300 Subject: [PATCH 86/93] daemon: add eval --- src/config.rs | 47 ++++++++--- src/daemon.rs | 219 +++++++++++++++++++++++++++++++++++++++++++++++--- src/system.rs | 1 + 3 files changed, 245 insertions(+), 22 deletions(-) diff --git a/src/config.rs b/src/config.rs index 2560806..c82169a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -155,7 +155,7 @@ impl PowerDelta { } } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(untagged, rename_all = "kebab-case")] pub enum Expression { #[serde(rename = "%cpu-usage")] @@ -184,12 +184,7 @@ pub enum Expression { #[serde(rename = "?on-battery")] OnBattery, - #[serde(rename = "#false")] - False, - - #[default] - #[serde(rename = "#true")] - True, + Boolean(bool), Number(f64), @@ -251,25 +246,49 @@ pub enum Expression { }, } +impl Default for Expression { + fn default() -> Self { + Self::Boolean(true) + } +} + +impl Expression { + pub fn as_number(&self) -> anyhow::Result { + let Self::Number(number) = self else { + bail!("tried to cast '{self:?}' to a number, failed") + }; + + Ok(*number) + } + + pub fn as_boolean(&self) -> anyhow::Result { + let Self::Boolean(boolean) = self else { + bail!("tried to cast '{self:?}' to a boolean, failed") + }; + + Ok(*boolean) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct Rule { - priority: u8, + pub priority: u8, #[serde(default, rename = "if", skip_serializing_if = "is_default")] - if_: Expression, + pub if_: Expression, #[serde(default, skip_serializing_if = "is_default")] - cpu: CpuDelta, + pub cpu: CpuDelta, #[serde(default, skip_serializing_if = "is_default")] - power: PowerDelta, + pub power: PowerDelta, } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(default, rename_all = "kebab-case")] pub struct DaemonConfig { #[serde(rename = "rule")] - rules: Vec, + pub rules: Vec, } impl DaemonConfig { @@ -278,7 +297,7 @@ impl DaemonConfig { format!("failed to read config from '{path}'", path = path.display()) })?; - let config: Self = toml::from_str(&contents).context("failed to parse config file")?; + let mut config: Self = toml::from_str(&contents).context("failed to parse config file")?; { let mut priorities = Vec::with_capacity(config.rules.len()); @@ -292,6 +311,8 @@ impl DaemonConfig { } } + config.rules.sort_by_key(|rule| rule.priority); + Ok(config) } } diff --git a/src/daemon.rs b/src/daemon.rs index 82c927b..35ddbed 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -5,6 +5,7 @@ use std::{ Arc, atomic::{AtomicBool, Ordering}, }, + thread, time::{Duration, Instant}, }; @@ -42,8 +43,8 @@ struct Daemon { /// The last computed polling interval. last_polling_interval: Option, - /// Whether if we are charging right now. - charging: bool, + /// The system state. + system: system::System, /// CPU usage and temperature log. cpu_log: VecDeque, @@ -52,6 +53,56 @@ struct Daemon { power_supply_log: VecDeque, } +impl Daemon { + fn rescan(&mut self) -> anyhow::Result<()> { + self.system.rescan()?; + + while self.cpu_log.len() > 99 { + self.cpu_log.pop_front(); + } + + self.cpu_log.push_back(CpuLog { + at: Instant::now(), + + usage: self + .system + .cpus + .iter() + .map(|cpu| cpu.stat.usage()) + .sum::() + / self.system.cpus.len() as f64, + + temperature: self.system.cpu_temperatures.values().sum::() + / self.system.cpu_temperatures.len() as f64, + }); + + let at = Instant::now(); + + let (charge_sum, charge_nr) = + self.system + .power_supplies + .iter() + .fold((0.0, 0u32), |(sum, count), power_supply| { + if let Some(charge_percent) = power_supply.charge_percent { + (sum + charge_percent, count + 1) + } else { + (sum, count) + } + }); + + while self.power_supply_log.len() > 99 { + self.power_supply_log.pop_front(); + } + + self.power_supply_log.push_back(PowerSupplyLog { + at, + charge: charge_sum / charge_nr as f64, + }); + + Ok(()) + } +} + struct CpuLog { at: Instant, @@ -134,12 +185,19 @@ struct PowerSupplyLog { } impl Daemon { + fn discharging(&self) -> bool { + self.system + .power_supplies + .iter() + .any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging")) + } + /// Calculates the discharge rate, returns a number between 0 and 1. /// /// The discharge rate is averaged per hour. /// So a return value of Some(0.3) means the battery has been /// discharging 30% per hour. - fn power_supply_discharge_rate(&mut self) -> Option { + fn power_supply_discharge_rate(&self) -> Option { let mut last_charge = None; // A list of increasing charge percentages. @@ -159,9 +217,7 @@ impl Daemon { }) .collect(); - self.charging = discharging.len() < 2; - - if self.charging { + if discharging.len() < 2 { return None; } @@ -183,7 +239,7 @@ impl Daemon { let mut interval = Duration::from_secs(5); // We are on battery, so we must be more conservative with our polling. - if !self.charging { + if self.discharging() { match self.power_supply_discharge_rate() { Some(discharge_rate) => { if discharge_rate > 0.2 { @@ -244,7 +300,124 @@ impl Daemon { } } +impl Daemon { + fn eval(&self, expression: &config::Expression) -> anyhow::Result> { + use config::Expression::*; + + macro_rules! try_ok { + ($expression:expr) => { + match $expression { + Some(value) => value, + None => return Ok(None), + } + }; + } + + Ok(Some(match expression { + CpuUsage => Number(self.cpu_log.back().unwrap().usage), + CpuUsageVolatility => Number(try_ok!(self.cpu_volatility()).usage), + CpuTemperature => Number(self.cpu_log.back().unwrap().temperature), + CpuTemperatureVolatility => Number(try_ok!(self.cpu_volatility()).temperature), + CpuIdleSeconds => Number(self.last_user_activity.elapsed().as_secs_f64()), + PowerSupplyCharge => Number(self.power_supply_log.back().unwrap().charge), + PowerSupplyDischargeRate => Number(try_ok!(self.power_supply_discharge_rate())), + + Charging => Boolean(!self.discharging()), + OnBattery => Boolean(self.discharging()), + + literal @ Boolean(_) | literal @ Number(_) => literal.clone(), + + Plus { value, plus } => Number( + try_ok!(self.eval(value)?).as_number()? + try_ok!(self.eval(plus)?).as_number()?, + ), + Minus { value, minus } => Number( + try_ok!(self.eval(value)?).as_number()? - try_ok!(self.eval(minus)?).as_number()?, + ), + Multiply { value, multiply } => Number( + try_ok!(self.eval(value)?).as_number()? + * try_ok!(self.eval(multiply)?).as_number()?, + ), + Power { value, power } => Number( + try_ok!(self.eval(value)?) + .as_number()? + .powf(try_ok!(self.eval(power)?).as_number()?), + ), + Divide { value, divide } => Number( + try_ok!(self.eval(value)?).as_number()? + / try_ok!(self.eval(divide)?).as_number()?, + ), + + LessThan { + value, + is_less_than, + } => Boolean( + try_ok!(self.eval(value)?).as_number()? + < try_ok!(self.eval(is_less_than)?).as_number()?, + ), + MoreThan { + value, + is_more_than, + } => Boolean( + try_ok!(self.eval(value)?).as_number()? + > try_ok!(self.eval(is_more_than)?).as_number()?, + ), + Equal { + value, + is_equal, + leeway, + } => { + let value = try_ok!(self.eval(value)?).as_number()?; + let leeway = try_ok!(self.eval(leeway)?).as_number()?; + + let is_equal = try_ok!(self.eval(is_equal)?).as_number()?; + + let minimum = value - leeway; + let maximum = value + leeway; + + Boolean(minimum < is_equal && is_equal < maximum) + } + + And { value, and } => Boolean( + try_ok!(self.eval(value)?).as_boolean()? + && try_ok!(self.eval(and)?).as_boolean()?, + ), + All { all } => { + let mut result = true; + + for value in all { + result = result && try_ok!(self.eval(value)?).as_boolean()?; + + if !result { + break; + } + } + + Boolean(result) + } + Or { value, or } => Boolean( + try_ok!(self.eval(value)?).as_boolean()? || try_ok!(self.eval(or)?).as_boolean()?, + ), + Any { any } => { + let mut result = false; + + for value in any { + result = result || try_ok!(self.eval(value)?).as_boolean()?; + + if result { + break; + } + } + + Boolean(result) + } + Not { not } => Boolean(!try_ok!(self.eval(not)?).as_boolean()?), + })) + } +} + pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { + assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); + log::info!("starting daemon..."); let cancelled = Arc::new(AtomicBool::new(false)); @@ -256,9 +429,37 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { }) .context("failed to set Ctrl-C handler")?; - let mut system = system::System::new()?; + let mut daemon = Daemon { + last_user_activity: Instant::now(), - while !cancelled.load(Ordering::SeqCst) {} + last_polling_interval: None, + + system: system::System::new()?, + + cpu_log: VecDeque::new(), + power_supply_log: VecDeque::new(), + }; + + while !cancelled.load(Ordering::SeqCst) { + daemon.rescan()?; + + let sleep_until = Instant::now() + daemon.polling_interval(); + + for rule in &config.rules { + let Some(condition) = daemon.eval(&rule.if_)? else { + continue; + }; + + if condition.as_boolean()? { + rule.cpu.apply()?; + rule.power.apply()?; + } + } + + if let Some(delay) = sleep_until.checked_duration_since(Instant::now()) { + thread::sleep(delay); + } + } log::info!("exiting..."); diff --git a/src/system.rs b/src/system.rs index 4a86893..ed43ad7 100644 --- a/src/system.rs +++ b/src/system.rs @@ -50,6 +50,7 @@ impl System { || self.is_desktop()?; self.rescan_load_average()?; + self.rescan_temperatures()?; Ok(()) } From 008e05b726459765665016d885e19ad07aba5398 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 20:42:15 +0300 Subject: [PATCH 87/93] cpu: use only recent cpu log entries for calculating volatility --- src/daemon.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 35ddbed..fc1a6a5 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,6 +1,5 @@ use std::{ collections::VecDeque, - ops, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -114,8 +113,6 @@ struct CpuLog { } struct CpuVolatility { - at: ops::Range, - usage: f64, temperature: f64, @@ -123,6 +120,17 @@ struct CpuVolatility { impl Daemon { fn cpu_volatility(&self) -> Option { + let recent_log_count = self + .cpu_log + .iter() + .rev() + .take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60)) + .count(); + + if recent_log_count < 2 { + return None; + } + if self.cpu_log.len() < 2 { return None; } @@ -142,8 +150,6 @@ impl Daemon { } Some(CpuVolatility { - at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at, - usage: usage_change_sum / change_count as f64, temperature: temperature_change_sum / change_count as f64, }) From a25ae59bdebfe67fa9b83f40298ba54a17f32736 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 20:46:07 +0300 Subject: [PATCH 88/93] package: rename to watt --- Cargo.lock | 116 ++----------- Cargo.toml | 26 ++- src/daemon_old.rs | 426 ---------------------------------------------- 3 files changed, 28 insertions(+), 540 deletions(-) delete mode 100644 src/daemon_old.rs diff --git a/Cargo.lock b/Cargo.lock index f077741..bb5a94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,27 +182,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys", -] - [[package]] name = "env_filter" version = "0.1.3" @@ -232,17 +211,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hashbrown" version = "0.15.3" @@ -301,12 +269,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", - "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", - "windows-sys", ] [[package]] @@ -320,37 +286,12 @@ dependencies = [ "syn", ] -[[package]] -name = "jiff-tzdb" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags", - "libc", -] - [[package]] name = "log" version = "0.4.27" @@ -391,12 +332,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "portable-atomic" version = "1.11.0" @@ -430,17 +365,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_users" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" version = "1.11.1" @@ -505,26 +429,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "superfreq" -version = "0.3.2" -dependencies = [ - "anyhow", - "clap", - "clap-verbosity-flag", - "ctrlc", - "derive_more", - "dirs", - "env_logger", - "jiff", - "log", - "num_cpus", - "serde", - "thiserror", - "toml", - "yansi", -] - [[package]] name = "syn" version = "2.0.101" @@ -622,10 +526,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +name = "watt" +version = "0.4.0" +dependencies = [ + "anyhow", + "clap", + "clap-verbosity-flag", + "ctrlc", + "derive_more", + "env_logger", + "log", + "num_cpus", + "serde", + "thiserror", + "toml", + "yansi", +] [[package]] name = "windows-sys" diff --git a/Cargo.toml b/Cargo.toml index aeecd4b..ecc84ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,21 @@ [package] -name = "superfreq" +name = "watt" description = "Modern CPU frequency and power management utility for Linux" -version = "0.3.2" +version = "0.4.0" edition = "2024" -authors = ["NotAShelf "] +authors = ["NotAShelf ", "RGBCube "] rust-version = "1.85" [dependencies] -serde = { version = "1.0", features = ["derive"] } -toml = "0.8" -dirs = "6.0" -clap = { version = "4.0", features = ["derive", "env"] } -num_cpus = "1.16" -ctrlc = "3.4" -log = "0.4" -env_logger = "0.11" -thiserror = "2.0" anyhow = "1.0" -jiff = "0.2.13" +clap = { version = "4.0", features = ["derive", "env"] } clap-verbosity-flag = "3.0.2" -yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } +ctrlc = "3.4" derive_more = { version = "2.0.1", features = ["full"] } +env_logger = "0.11" +log = "0.4" +num_cpus = "1.16" +serde = { version = "1.0", features = ["derive"] } +thiserror = "2.0" +toml = "0.8" +yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] } diff --git a/src/daemon_old.rs b/src/daemon_old.rs deleted file mode 100644 index 3a20cb4..0000000 --- a/src/daemon_old.rs +++ /dev/null @@ -1,426 +0,0 @@ -use anyhow::Context; -use anyhow::bail; - -use crate::config::AppConfig; -use crate::core::SystemReport; -use crate::engine; -use crate::monitor; -use std::collections::VecDeque; -use std::fs::File; -use std::io::Write; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, Instant}; - -/// Tracks historical system data for "advanced" adaptive polling -#[derive(Debug)] -struct SystemHistory { - /// Last several CPU usage measurements - cpu_usage_history: VecDeque, - /// Last several temperature readings - temperature_history: VecDeque, - /// Time of last detected user activity - last_user_activity: Instant, - /// Previous battery percentage (to calculate discharge rate) - last_battery_percentage: Option, - /// Timestamp of last battery reading - last_battery_timestamp: Option, - /// Battery discharge rate (%/hour) - battery_discharge_rate: Option, - /// Time spent in each system state - state_durations: std::collections::HashMap, - /// Last time a state transition happened - last_state_change: Instant, - /// Current system state - current_state: SystemState, - /// Last computed optimal polling interval - last_computed_interval: Option, -} - -impl SystemHistory { - /// Update system history with new report data - fn update(&mut self, report: &SystemReport) { - // Update CPU usage history - if !report.cpu_cores.is_empty() { - let mut total_usage: f32 = 0.0; - let mut core_count: usize = 0; - - for core in &report.cpu_cores { - if let Some(usage) = core.usage_percent { - total_usage += usage; - core_count += 1; - } - } - - if core_count > 0 { - let avg_usage = total_usage / core_count as f32; - - // Keep only the last 5 measurements - if self.cpu_usage_history.len() >= 5 { - self.cpu_usage_history.pop_front(); - } - self.cpu_usage_history.push_back(avg_usage); - - // Update last_user_activity if CPU usage indicates activity - // Consider significant CPU usage or sudden change as user activity - if avg_usage > 20.0 - || (self.cpu_usage_history.len() > 1 - && (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2]) - .abs() - > 15.0) - { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on CPU usage"); - } - } - } - - // Update temperature history - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if self.temperature_history.len() >= 5 { - self.temperature_history.pop_front(); - } - self.temperature_history.push_back(temp); - - // Significant temperature increase can indicate user activity - if self.temperature_history.len() > 1 { - let temp_change = - temp - self.temperature_history[self.temperature_history.len() - 2]; - if temp_change > 5.0 { - // 5°C rise in temperature - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on temperature change"); - } - } - } - - // Update battery discharge rate - if let Some(battery) = report.batteries.first() { - // Reset when we are charging or have just connected AC - if battery.ac_connected { - // Reset discharge tracking but continue updating the rest of - // the history so we still detect activity/load changes on AC. - self.battery_discharge_rate = None; - self.last_battery_percentage = None; - self.last_battery_timestamp = None; - } - - if let Some(current_percentage) = battery.capacity_percent { - let current_percent = f32::from(current_percentage); - - if let (Some(last_percentage), Some(last_timestamp)) = - (self.last_battery_percentage, self.last_battery_timestamp) - { - let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0; - // Only calculate discharge rate if at least 30 seconds have passed - // and we're not on AC power - if elapsed_hours > 0.0083 && !battery.ac_connected { - // 0.0083 hours = 30 seconds - // Calculate discharge rate in percent per hour - let percent_change = last_percentage - current_percent; - if percent_change > 0.0 { - // Only if battery is discharging - let hourly_rate = percent_change / elapsed_hours; - // Clamp the discharge rate to a reasonable maximum value (100%/hour) - let clamped_rate = hourly_rate.min(100.0); - self.battery_discharge_rate = Some(clamped_rate); - } - } - } - - self.last_battery_percentage = Some(current_percent); - self.last_battery_timestamp = Some(Instant::now()); - } - } - - // Update system state tracking - let new_state = determine_system_state(report, self); - if new_state != self.current_state { - // Record time spent in previous state - let time_in_state = self.last_state_change.elapsed(); - *self - .state_durations - .entry(self.current_state.clone()) - .or_insert(Duration::ZERO) += time_in_state; - - // State changes (except to Idle) likely indicate user activity - if new_state != SystemState::Idle && new_state != SystemState::LowLoad { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system state change to {new_state:?}"); - } - - // Update state - self.current_state = new_state; - self.last_state_change = Instant::now(); - } - - // Check for significant load changes - if report.system_load.load_avg_1min > 1.0 { - self.last_user_activity = Instant::now(); - log::debug!("User activity detected based on system load"); - } - } - - /// Calculate CPU usage volatility (how much it's changing) - fn get_cpu_volatility(&self) -> f32 { - if self.cpu_usage_history.len() < 2 { - return 0.0; - } - - let mut sum_of_changes = 0.0; - for i in 1..self.cpu_usage_history.len() { - sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs(); - } - - sum_of_changes / (self.cpu_usage_history.len() - 1) as f32 - } - - /// Calculate temperature volatility - fn get_temperature_volatility(&self) -> f32 { - if self.temperature_history.len() < 2 { - return 0.0; - } - - let mut sum_of_changes = 0.0; - for i in 1..self.temperature_history.len() { - sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs(); - } - - sum_of_changes / (self.temperature_history.len() - 1) as f32 - } - - /// Determine if the system appears to be idle - fn is_system_idle(&self) -> bool { - if self.cpu_usage_history.is_empty() { - return false; - } - - // System considered idle if the average CPU usage of last readings is below 10% - let recent_avg = - self.cpu_usage_history.iter().sum::() / self.cpu_usage_history.len() as f32; - recent_avg < 10.0 && self.get_cpu_volatility() < 5.0 - } -} - -/// Run the daemon -pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> { - log::info!("Starting superfreq daemon..."); - - // Validate critical configuration values before proceeding - validate_poll_intervals( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - )?; - - // Create a flag that will be set to true when a signal is received - let running = Arc::new(AtomicBool::new(true)); - let r = running.clone(); - - // Set up signal handlers - ctrlc::set_handler(move || { - log::info!("Received shutdown signal, exiting..."); - r.store(false, Ordering::SeqCst); - }) - .context("failed to set Ctrl-C handler")?; - - 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 { - 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 { - log::warn!( - "Poll interval is set to zero in config, using 1s minimum to prevent a busy loop" - ); - } - let mut system_history = SystemHistory::default(); - - // Main loop - while running.load(Ordering::SeqCst) { - let start_time = Instant::now(); - - match monitor::collect_system_report(&config) { - Ok(report) => { - log::debug!("Collected system report, applying settings..."); - - // Store the current state before updating history - let previous_state = system_history.current_state.clone(); - - // Update system history with new data - system_history.update(&report); - - // 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) { - log::error!("Failed to write stats file: {e}"); - } - } - - match engine::determine_and_apply_settings(&report, &config, None) { - Ok(()) => { - log::debug!("Successfully applied system settings"); - - // If system state changed, log the new state - if system_history.current_state != previous_state { - log::info!( - "System state changed to: {:?}", - system_history.current_state - ); - } - } - Err(e) => { - log::error!("Error applying system settings: {e}"); - } - } - - // Check if we're on battery - let on_battery = !report.batteries.is_empty() - && report.batteries.first().is_some_and(|b| !b.ac_connected); - - // Calculate optimal polling interval if adaptive polling is enabled - if config.daemon.adaptive_interval { - match system_history.calculate_optimal_interval(&config, on_battery) { - Ok(optimal_interval) => { - // Store the new interval - system_history.last_computed_interval = Some(optimal_interval); - - log::debug!("Recalculated optimal interval: {optimal_interval}s"); - - // Don't change the interval too dramatically at once - match optimal_interval.cmp(¤t_poll_interval) { - std::cmp::Ordering::Greater => { - current_poll_interval = - (current_poll_interval + optimal_interval) / 2; - } - std::cmp::Ordering::Less => { - current_poll_interval = current_poll_interval - - ((current_poll_interval - optimal_interval) / 2).max(1); - } - std::cmp::Ordering::Equal => { - // No change needed when they're equal - } - } - } - Err(e) => { - // Log the error and stop the daemon when an invalid configuration is detected - log::error!("Critical configuration error: {e}"); - running.store(false, Ordering::SeqCst); - break; - } - } - - // Make sure that we respect the (user) configured min and max limits - current_poll_interval = current_poll_interval.clamp( - config.daemon.min_poll_interval_sec, - config.daemon.max_poll_interval_sec, - ); - - 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 { - let battery_multiplier = 2; // poll half as often on battery - - // We need to make sure `poll_interval_sec` is *at least* 1 - // before multiplying. - let safe_interval = config.daemon.poll_interval_sec.max(1); - current_poll_interval = (safe_interval * battery_multiplier) - .min(config.daemon.max_poll_interval_sec); - - 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 { - log::debug!( - "Using minimum poll interval of 1s instead of configured 0s" - ); - } - } - } - } - Err(e) => { - log::error!("Error collecting system report: {e}"); - } - } - - // Sleep for the remaining time in the poll interval - let elapsed = start_time.elapsed(); - let poll_duration = Duration::from_secs(current_poll_interval); - if elapsed < poll_duration { - let sleep_time = poll_duration - elapsed; - log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs()); - std::thread::sleep(sleep_time); - } - } - - log::info!("Daemon stopped"); - Ok(()) -} - -/// Simplified system state used for determining when to adjust polling interval -#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] -enum SystemState { - #[default] - Unknown, - OnAC, - OnBattery, - HighLoad, - LowLoad, - HighTemp, - Idle, -} - -/// Determine the current system state for adaptive polling -fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState { - // Check power state first - if !report.batteries.is_empty() { - if let Some(battery) = report.batteries.first() { - if battery.ac_connected { - return SystemState::OnAC; - } - return SystemState::OnBattery; - } - } - - // No batteries means desktop, so always AC - if report.batteries.is_empty() { - return SystemState::OnAC; - } - - // Check temperature - if let Some(temp) = report.cpu_global.average_temperature_celsius { - if temp > 80.0 { - return SystemState::HighTemp; - } - } - - // Check load first, as high load should take precedence over idle state - let avg_load = report.system_load.load_avg_1min; - if avg_load > 3.0 { - return SystemState::HighLoad; - } - - // Check idle state only if we don't have high load - if history.is_system_idle() { - return SystemState::Idle; - } - - // Check for low load - if avg_load < 0.5 { - return SystemState::LowLoad; - } - - // Default case - SystemState::Unknown -} From b6dd9e78d42b9fe90bfba456bc859476860b8925 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 21:09:35 +0300 Subject: [PATCH 89/93] package: make multicall --- build.rs | 51 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 63 +++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 build.rs diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5cc203d --- /dev/null +++ b/build.rs @@ -0,0 +1,51 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +const MULTICALL_NAMES: &[&str] = &["cpu", "power"]; + +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=build.rs"); + + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let target = out_dir + .parent() // target/debug/build/-/out + .and_then(|p| p.parent()) // target/debug/build/- + .and_then(|p| p.parent()) // target/debug/ + .ok_or("failed to find target directory")?; + + let main_binary_name = env::var("CARGO_PKG_NAME")?; + + let main_binary_path = target.join(&main_binary_name); + + let mut errored = false; + + for name in MULTICALL_NAMES { + let hardlink_path = target.join(name); + + if hardlink_path.exists() { + if hardlink_path.is_dir() { + fs::remove_dir_all(&hardlink_path)?; + } else { + fs::remove_file(&hardlink_path)?; + } + } + + if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) { + println!( + "cargo:warning=failed to create hard link '{path}': {error}", + path = hardlink_path.display(), + ); + errored = true; + } + } + + if errored { + println!( + "cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again" + ); + println!("cargo:warning=keep in mind that this is for development purposes only"); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e435cee..5b56d92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,30 +21,52 @@ use yansi::Paint as _; #[derive(clap::Parser, Debug)] #[clap(author, version, about)] struct Cli { - #[command(flatten)] - verbosity: clap_verbosity_flag::Verbosity, - #[clap(subcommand)] command: Command, } #[derive(clap::Parser, Debug)] +#[clap(multicall = true)] enum Command { - /// Display information. - Info, + /// Watt daemon. + Watt { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, - /// Start the daemon. - Start { /// The daemon config path. #[arg(long, env = "WATT_CONFIG")] config: PathBuf, }, - /// Modify CPU attributes. - CpuSet(config::CpuDelta), + /// CPU metadata and modification utility. + Cpu { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, + #[clap(subcommand)] + command: CpuCommand, + }, + + /// Power supply metadata and modification utility. + Power { + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, + + #[clap(subcommand)] + command: PowerCommand, + }, +} + +#[derive(clap::Parser, Debug)] +enum CpuCommand { + /// Modify CPU attributes. + Set(config::CpuDelta), +} + +#[derive(clap::Parser, Debug)] +enum PowerCommand { /// Modify power supply attributes. - PowerSet(config::PowerDelta), + Set(config::PowerDelta), } fn real_main() -> anyhow::Result<()> { @@ -52,24 +74,33 @@ fn real_main() -> anyhow::Result<()> { yansi::whenever(yansi::Condition::TTY_AND_COLOR); + let (Command::Watt { verbosity, .. } + | Command::Cpu { verbosity, .. } + | Command::Power { verbosity, .. }) = cli.command; + env_logger::Builder::new() - .filter_level(cli.verbosity.log_level_filter()) + .filter_level(verbosity.log_level_filter()) .format_timestamp(None) .format_module_path(false) .init(); match cli.command { - Command::Info => todo!(), - - Command::Start { config } => { + Command::Watt { config, .. } => { let config = config::DaemonConfig::load_from(&config) .context("failed to load daemon config file")?; daemon::run(config) } - Command::CpuSet(delta) => delta.apply(), - Command::PowerSet(delta) => delta.apply(), + Command::Cpu { + command: CpuCommand::Set(delta), + .. + } => delta.apply(), + + Command::Power { + command: PowerCommand::Set(delta), + .. + } => delta.apply(), } } From 917ed77255ade0b51d4677f934d707870a3622ab Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 21:33:32 +0300 Subject: [PATCH 90/93] config: fix serde --- config.toml | 1 + src/config.rs | 75 ++++++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 4 +-- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/config.toml b/config.toml index 2f796b7..04159cd 100644 --- a/config.toml +++ b/config.toml @@ -1,2 +1,3 @@ [[rule]] priority = 0 +if = { value = "%cpu-usage", is-more-than = 0.7 } diff --git a/src/config.rs b/src/config.rs index c82169a..365970e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -155,33 +155,79 @@ impl PowerDelta { } } +macro_rules! named { + ($variant:ident => $value:literal) => { + pub mod $variant { + pub fn serialize(serializer: S) -> Result { + serializer.serialize_str($value) + } + + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result<(), D::Error> { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = (); + + fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result { + writer.write_str(concat!("\"", $value, "\"")) + } + + fn visit_str(self, value: &str) -> Result { + if value != $value { + return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)); + } + + Ok(()) + } + } + + deserializer.deserialize_str(Visitor) + } + } + }; +} + +mod expression { + named!(cpu_usage => "%cpu-usage"); + named!(cpu_usage_volatility => "$cpu-usage-volatility"); + named!(cpu_temperature => "$cpu-temperature"); + named!(cpu_temperature_volatility => "$cpu-temperature-volatility"); + named!(cpu_idle_seconds => "$cpu-idle-seconds"); + named!(power_supply_charge => "%power-supply-charge"); + named!(power_supply_discharge_rate => "%power-supply-discharge-rate"); + named!(charging => "?charging"); + named!(on_battery => "?on-battery"); +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(untagged, rename_all = "kebab-case")] +#[serde(untagged)] pub enum Expression { - #[serde(rename = "%cpu-usage")] + #[serde(with = "expression::cpu_usage")] CpuUsage, - #[serde(rename = "$cpu-usage-volatility")] + #[serde(with = "expression::cpu_usage_volatility")] CpuUsageVolatility, - #[serde(rename = "$cpu-temperature")] + #[serde(with = "expression::cpu_temperature")] CpuTemperature, - #[serde(rename = "$cpu-temperature-volatility")] + #[serde(with = "expression::cpu_temperature_volatility")] CpuTemperatureVolatility, - #[serde(rename = "$cpu-idle-seconds")] + #[serde(with = "expression::cpu_idle_seconds")] CpuIdleSeconds, - #[serde(rename = "%power-supply-charge")] + #[serde(with = "expression::power_supply_charge")] PowerSupplyCharge, - #[serde(rename = "%power-supply-discharge-rate")] + #[serde(with = "expression::power_supply_discharge_rate")] PowerSupplyDischargeRate, - #[serde(rename = "?charging")] + #[serde(with = "expression::charging")] Charging, - #[serde(rename = "?on-battery")] + #[serde(with = "expression::on_battery")] OnBattery, Boolean(bool), @@ -209,16 +255,18 @@ pub enum Expression { divide: Box, }, + #[serde(rename_all = "kebab-case")] LessThan { value: Box, is_less_than: Box, }, - + #[serde(rename_all = "kebab-case")] MoreThan { value: Box, is_more_than: Box, }, + #[serde(rename_all = "kebab-case")] Equal { value: Box, is_equal: Box, @@ -297,7 +345,8 @@ impl DaemonConfig { format!("failed to read config from '{path}'", path = path.display()) })?; - let mut config: Self = toml::from_str(&contents).context("failed to parse config file")?; + let mut config: Self = toml::from_str(&contents) + .with_context(|| format!("failed to parse file at '{path}'", path = path.display(),))?; { let mut priorities = Vec::with_capacity(config.rules.len()); @@ -313,6 +362,8 @@ impl DaemonConfig { config.rules.sort_by_key(|rule| rule.priority); + log::debug!("loaded config: {config:#?}"); + Ok(config) } } diff --git a/src/main.rs b/src/main.rs index 5b56d92..feed86a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,8 +86,8 @@ fn real_main() -> anyhow::Result<()> { match cli.command { Command::Watt { config, .. } => { - let config = config::DaemonConfig::load_from(&config) - .context("failed to load daemon config file")?; + let config = + config::DaemonConfig::load_from(&config).context("failed to load daemon config")?; daemon::run(config) } From 2c154cd589b5afdfb0d1b59a07ba142c96194025 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 22:50:22 +0300 Subject: [PATCH 91/93] expr: strict eval --- src/config.rs | 189 ++++++++++++++++++++++++++++++++++++++++++-------- src/daemon.rs | 128 ++++------------------------------ 2 files changed, 172 insertions(+), 145 deletions(-) diff --git a/src/config.rs b/src/config.rs index 365970e..36cc82c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -195,10 +195,11 @@ mod expression { named!(cpu_temperature => "$cpu-temperature"); named!(cpu_temperature_volatility => "$cpu-temperature-volatility"); named!(cpu_idle_seconds => "$cpu-idle-seconds"); + named!(power_supply_charge => "%power-supply-charge"); named!(power_supply_discharge_rate => "%power-supply-discharge-rate"); - named!(charging => "?charging"); - named!(on_battery => "?on-battery"); + + named!(discharging => "?discharging"); } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -225,65 +226,80 @@ pub enum Expression { #[serde(with = "expression::power_supply_discharge_rate")] PowerSupplyDischargeRate, - #[serde(with = "expression::charging")] - Charging, - #[serde(with = "expression::on_battery")] - OnBattery, + #[serde(with = "expression::discharging")] + Discharging, Boolean(bool), Number(f64), Plus { - value: Box, - plus: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "plus")] + b: Box, }, Minus { - value: Box, - minus: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "minus")] + b: Box, }, Multiply { - value: Box, - multiply: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "multiply")] + b: Box, }, Power { - value: Box, - power: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "power")] + b: Box, }, Divide { - value: Box, - divide: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "divide")] + b: Box, }, - #[serde(rename_all = "kebab-case")] LessThan { - value: Box, - is_less_than: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-less-than")] + b: Box, }, - #[serde(rename_all = "kebab-case")] MoreThan { - value: Box, - is_more_than: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-more-than")] + b: Box, }, - #[serde(rename_all = "kebab-case")] Equal { - value: Box, - is_equal: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "is-equal")] + b: Box, leeway: Box, }, And { - value: Box, - and: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "and")] + b: Box, }, All { all: Vec, }, Or { - value: Box, - or: Box, + #[serde(rename = "value")] + a: Box, + #[serde(rename = "or")] + b: Box, }, Any { any: Vec, @@ -318,6 +334,121 @@ impl Expression { } } +#[derive(Debug, Clone, PartialEq)] +pub struct EvalState { + pub cpu_usage: f64, + pub cpu_usage_volatility: Option, + pub cpu_temperature: f64, + pub cpu_temperature_volatility: Option, + pub cpu_idle_seconds: f64, + + pub power_supply_charge: f64, + pub power_supply_discharge_rate: Option, + + pub discharging: bool, +} + +impl Expression { + pub fn eval(&self, state: &EvalState) -> anyhow::Result> { + use Expression::*; + + macro_rules! try_ok { + ($expression:expr) => { + match $expression { + Some(value) => value, + None => return Ok(None), + } + }; + } + + macro_rules! eval { + ($expression:expr) => { + try_ok!($expression.eval(state)?) + }; + } + + // [e8dax09]: This may be look inefficient, and it definitely isn't optimal, + // but expressions in rules are usually so small that it doesn't matter or + // make a perceiveable performance difference. + // + // We also want to be strict, instead of lazy in binary operations, because + // we want to catch type errors immediately. + // + // FIXME: We currently cannot catch errors that will happen when propagating None. + // You can have a type error go uncaught on first startup by using $cpu-usage-volatility + // incorrectly, for example. + Ok(Some(match self { + CpuUsage => Number(state.cpu_usage), + CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)), + CpuTemperature => Number(state.cpu_temperature), + CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)), + CpuIdleSeconds => Number(state.cpu_idle_seconds), + + PowerSupplyCharge => Number(state.cpu_idle_seconds), + PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)), + + Discharging => Boolean(state.discharging), + + literal @ (Boolean(_) | Number(_)) => literal.clone(), + + Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?), + Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?), + Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?), + Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)), + Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?), + + LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?), + MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?), + Equal { a, b, leeway } => { + let a = eval!(a).as_number()?; + let b = eval!(b).as_number()?; + let leeway = eval!(leeway).as_number()?; + + let minimum = a - leeway; + let maximum = a + leeway; + + Boolean(minimum < b && b < maximum) + } + + And { a, b } => { + let a = eval!(a).as_boolean()?; + let b = eval!(b).as_boolean()?; + + Boolean(a && b) + } + All { all } => { + let mut result = true; + + for value in all { + let value = eval!(value).as_boolean()?; + + result = result && value; + } + + Boolean(result) + } + Or { a, b } => { + let a = eval!(a).as_boolean()?; + let b = eval!(b).as_boolean()?; + + Boolean(a || b) + } + Any { any } => { + let mut result = false; + + for value in any { + let value = eval!(value).as_boolean()?; + + result = result || value; + } + + Boolean(result) + } + Not { not } => Boolean(!eval!(not).as_boolean()?), + })) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct Rule { diff --git a/src/daemon.rs b/src/daemon.rs index fc1a6a5..43cac22 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -306,121 +306,6 @@ impl Daemon { } } -impl Daemon { - fn eval(&self, expression: &config::Expression) -> anyhow::Result> { - use config::Expression::*; - - macro_rules! try_ok { - ($expression:expr) => { - match $expression { - Some(value) => value, - None => return Ok(None), - } - }; - } - - Ok(Some(match expression { - CpuUsage => Number(self.cpu_log.back().unwrap().usage), - CpuUsageVolatility => Number(try_ok!(self.cpu_volatility()).usage), - CpuTemperature => Number(self.cpu_log.back().unwrap().temperature), - CpuTemperatureVolatility => Number(try_ok!(self.cpu_volatility()).temperature), - CpuIdleSeconds => Number(self.last_user_activity.elapsed().as_secs_f64()), - PowerSupplyCharge => Number(self.power_supply_log.back().unwrap().charge), - PowerSupplyDischargeRate => Number(try_ok!(self.power_supply_discharge_rate())), - - Charging => Boolean(!self.discharging()), - OnBattery => Boolean(self.discharging()), - - literal @ Boolean(_) | literal @ Number(_) => literal.clone(), - - Plus { value, plus } => Number( - try_ok!(self.eval(value)?).as_number()? + try_ok!(self.eval(plus)?).as_number()?, - ), - Minus { value, minus } => Number( - try_ok!(self.eval(value)?).as_number()? - try_ok!(self.eval(minus)?).as_number()?, - ), - Multiply { value, multiply } => Number( - try_ok!(self.eval(value)?).as_number()? - * try_ok!(self.eval(multiply)?).as_number()?, - ), - Power { value, power } => Number( - try_ok!(self.eval(value)?) - .as_number()? - .powf(try_ok!(self.eval(power)?).as_number()?), - ), - Divide { value, divide } => Number( - try_ok!(self.eval(value)?).as_number()? - / try_ok!(self.eval(divide)?).as_number()?, - ), - - LessThan { - value, - is_less_than, - } => Boolean( - try_ok!(self.eval(value)?).as_number()? - < try_ok!(self.eval(is_less_than)?).as_number()?, - ), - MoreThan { - value, - is_more_than, - } => Boolean( - try_ok!(self.eval(value)?).as_number()? - > try_ok!(self.eval(is_more_than)?).as_number()?, - ), - Equal { - value, - is_equal, - leeway, - } => { - let value = try_ok!(self.eval(value)?).as_number()?; - let leeway = try_ok!(self.eval(leeway)?).as_number()?; - - let is_equal = try_ok!(self.eval(is_equal)?).as_number()?; - - let minimum = value - leeway; - let maximum = value + leeway; - - Boolean(minimum < is_equal && is_equal < maximum) - } - - And { value, and } => Boolean( - try_ok!(self.eval(value)?).as_boolean()? - && try_ok!(self.eval(and)?).as_boolean()?, - ), - All { all } => { - let mut result = true; - - for value in all { - result = result && try_ok!(self.eval(value)?).as_boolean()?; - - if !result { - break; - } - } - - Boolean(result) - } - Or { value, or } => Boolean( - try_ok!(self.eval(value)?).as_boolean()? || try_ok!(self.eval(or)?).as_boolean()?, - ), - Any { any } => { - let mut result = false; - - for value in any { - result = result || try_ok!(self.eval(value)?).as_boolean()?; - - if result { - break; - } - } - - Boolean(result) - } - Not { not } => Boolean(!try_ok!(self.eval(not)?).as_boolean()?), - })) - } -} - pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { assert!(config.rules.is_sorted_by_key(|rule| rule.priority)); @@ -451,8 +336,19 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { let sleep_until = Instant::now() + daemon.polling_interval(); + let state = config::EvalState { + cpu_usage: daemon.cpu_log.back().unwrap().usage, + cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage), + cpu_temperature: daemon.cpu_log.back().unwrap().temperature, + cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature), + cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(), + power_supply_charge: daemon.power_supply_log.back().unwrap().charge, + power_supply_discharge_rate: daemon.power_supply_discharge_rate(), + discharging: daemon.discharging(), + }; + for rule in &config.rules { - let Some(condition) = daemon.eval(&rule.if_)? else { + let Some(condition) = rule.if_.eval(&state)? else { continue; }; From 0e8b40227bf07a275a49fe0332c31cef6d6b00d5 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Wed, 4 Jun 2025 23:50:19 +0300 Subject: [PATCH 92/93] config: rename rule condition field --- src/config.rs | 2 +- src/daemon.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 36cc82c..b3d214e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -455,7 +455,7 @@ pub struct Rule { pub priority: u8, #[serde(default, rename = "if", skip_serializing_if = "is_default")] - pub if_: Expression, + pub condition: Expression, #[serde(default, skip_serializing_if = "is_default")] pub cpu: CpuDelta, diff --git a/src/daemon.rs b/src/daemon.rs index 43cac22..641d1cf 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -348,7 +348,7 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { }; for rule in &config.rules { - let Some(condition) = rule.if_.eval(&state)? else { + let Some(condition) = rule.condition.eval(&state)? else { continue; }; From 3e235c089bf10cf6b6fe32914d99d93cb9ce80bc Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 5 Jun 2025 00:09:39 +0300 Subject: [PATCH 93/93] daemon: merge cpu deltas --- src/daemon.rs | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 641d1cf..9e0370a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,5 +1,7 @@ use std::{ - collections::VecDeque, + cell::LazyCell, + collections::{HashMap, VecDeque}, + ops::Deref, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -347,17 +349,56 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> { discharging: daemon.discharging(), }; + let mut cpu_delta_for = HashMap::::new(); + let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::>()); + for rule in &config.rules { let Some(condition) = rule.condition.eval(&state)? else { continue; }; + let cpu_for = rule.cpu.for_.as_ref().unwrap_or_else(|| &*all_cpus); + + for cpu in cpu_for { + let delta = cpu_delta_for.entry(*cpu).or_default(); + + delta.for_ = Some(vec![*cpu]); + + if let Some(governor) = rule.cpu.governor.as_ref() { + delta.governor = Some(governor.clone()); + } + + if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() { + delta.energy_performance_preference = Some(epp.clone()); + } + + if let Some(epb) = rule.cpu.energy_performance_bias.as_ref() { + delta.energy_performance_bias = Some(epb.clone()); + } + + if let Some(mhz_minimum) = rule.cpu.frequency_mhz_minimum { + delta.frequency_mhz_minimum = Some(mhz_minimum); + } + + if let Some(mhz_maximum) = rule.cpu.frequency_mhz_maximum { + delta.frequency_mhz_maximum = Some(mhz_maximum); + } + + if let Some(turbo) = rule.cpu.turbo { + delta.turbo = Some(turbo); + } + } + + // TODO: Also merge this into one like CPU. if condition.as_boolean()? { - rule.cpu.apply()?; rule.power.apply()?; } } + for delta in cpu_delta_for.values() { + delta.apply()?; + } + if let Some(delay) = sleep_until.checked_duration_since(Instant::now()) { thread::sleep(delay); }