From 498d179aa8e54701ea54795aa7ce6122defe2843 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 14 May 2025 01:38:55 +0300 Subject: [PATCH] config: improve watcher; debounce --- src/config/load.rs | 104 ++++++++++++++++++++++++++---------------- src/config/mod.rs | 4 +- src/config/types.rs | 14 +++--- src/config/watcher.rs | 80 ++++++++++++++++++++------------ src/cpu.rs | 12 ++--- src/daemon.rs | 74 ++++++++++++++++++++---------- src/engine.rs | 3 +- src/monitor.rs | 5 +- 8 files changed, 186 insertions(+), 110 deletions(-) diff --git a/src/config/load.rs b/src/config/load.rs index 4e463b5..f0fedce 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -1,18 +1,43 @@ // Configuration loading functionality use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig}; -// The primary function to load application configuration. -// It tries user-specific and then system-wide TOML files. -// Falls back to default settings if no file is found or if parsing fails. +/// 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() -> 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 { + // 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::IoError(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Specified config file not found: {}", path.display()), + ))); + } + + // Otherwise try the standard paths let mut config_paths: Vec = Vec::new(); // User-specific path if let Some(home_dir) = dirs::home_dir() { - let user_config_path = home_dir.join(".config/auto_cpufreq_rs/config.toml"); + let user_config_path = home_dir.join(".config/superfreq/config.toml"); config_paths.push(user_config_path); } else { eprintln!( @@ -20,43 +45,18 @@ pub fn load_config() -> Result { ); } - // System-wide path - let system_config_path = PathBuf::from("/etc/auto_cpufreq_rs/config.toml"); - config_paths.push(system_config_path); + // System-wide paths + config_paths.push(PathBuf::from("/etc/superfreq/config.toml")); + config_paths.push(PathBuf::from("/etc/superfreq.toml")); for path in config_paths { if path.exists() { - println!("Attempting to load config from: {}", path.display()); - match fs::read_to_string(&path) { - Ok(contents) => { - match toml::from_str::(&contents) { - Ok(toml_app_config) => { - // Convert AppConfigToml to AppConfig - let app_config = AppConfig { - charger: ProfileConfig::from(toml_app_config.charger), - battery: ProfileConfig::from(toml_app_config.battery), - battery_charge_thresholds: toml_app_config.battery_charge_thresholds, - ignored_power_supplies: toml_app_config.ignored_power_supplies, - poll_interval_sec: toml_app_config.poll_interval_sec, - 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, - }, - }; - return Ok(app_config); - } - Err(e) => { - eprintln!("Error parsing config file {}: {}", path.display(), e); - } - } - } + println!("Loading config from: {}", path.display()); + match load_and_parse_config(&path) { + Ok(config) => return Ok(config), Err(e) => { - eprintln!("Error reading config file {}: {}", path.display(), e); + eprintln!("Error with config file {}: {}", path.display(), e); + // Continue trying other files } } } @@ -73,4 +73,30 @@ pub fn load_config() -> Result { poll_interval_sec: default_toml_config.poll_interval_sec, daemon: DaemonConfig::default(), }) -} \ No newline at end of file +} + +/// Load and parse a configuration file +fn load_and_parse_config(path: &Path) -> Result { + let contents = fs::read_to_string(path).map_err(ConfigError::IoError)?; + + let toml_app_config = + toml::from_str::(&contents).map_err(ConfigError::TomlError)?; + + // Convert AppConfigToml to AppConfig + Ok(AppConfig { + charger: ProfileConfig::from(toml_app_config.charger), + battery: ProfileConfig::from(toml_app_config.battery), + battery_charge_thresholds: toml_app_config.battery_charge_thresholds, + ignored_power_supplies: toml_app_config.ignored_power_supplies, + poll_interval_sec: toml_app_config.poll_interval_sec, + 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 index 3524422..b386b52 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,9 +1,9 @@ pub mod watcher; // Re-export all configuration types and functions -pub use self::types::*; pub use self::load::*; +pub use self::types::*; // Internal organization of config submodules +mod load; mod types; -mod load; \ No newline at end of file diff --git a/src/config/types.rs b/src/config/types.rs index 8342552..3f4f8ed 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -51,29 +51,29 @@ const fn default_poll_interval_sec() -> u64 { // Error type for config loading #[derive(Debug)] pub enum ConfigError { - Io(std::io::Error), - Toml(toml::de::Error), + IoError(std::io::Error), + TomlError(toml::de::Error), NoValidConfigFound, HomeDirNotFound, } impl From for ConfigError { fn from(err: std::io::Error) -> Self { - Self::Io(err) + Self::IoError(err) } } impl From for ConfigError { fn from(err: toml::de::Error) -> Self { - Self::Toml(err) + Self::TomlError(err) } } impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Io(e) => write!(f, "I/O error: {e}"), - Self::Toml(e) => write!(f, "TOML parsing error: {e}"), + Self::IoError(e) => write!(f, "I/O error: {e}"), + Self::TomlError(e) => write!(f, "TOML parsing error: {e}"), Self::NoValidConfigFound => write!(f, "No valid configuration file found."), Self::HomeDirNotFound => write!(f, "Could not determine user home directory."), } @@ -273,4 +273,4 @@ impl Default for DaemonConfigToml { stats_file_path: default_stats_file_path(), } } -} \ No newline at end of file +} diff --git a/src/config/watcher.rs b/src/config/watcher.rs index 78ece3c..8998281 100644 --- a/src/config/watcher.rs +++ b/src/config/watcher.rs @@ -1,17 +1,18 @@ use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use std::path::Path; -use std::sync::mpsc::{channel, Receiver}; -use std::time::Duration; -use std::thread; use std::error::Error; +use std::path::Path; +use std::sync::mpsc::{Receiver, TryRecvError, channel}; +use std::thread; +use std::time::Duration; -use crate::config::{load_config, AppConfig}; +use crate::config::{AppConfig, load_config_from_path}; /// Watches a configuration file for changes and reloads it when modified pub struct ConfigWatcher { rx: Receiver>, _watcher: RecommendedWatcher, // keep watcher alive while watching config_path: String, + last_event_time: std::time::Instant, } impl ConfigWatcher { @@ -29,6 +30,7 @@ impl ConfigWatcher { rx, _watcher: watcher, config_path: config_path.to_string(), + last_event_time: std::time::Instant::now(), }) } @@ -36,33 +38,53 @@ impl ConfigWatcher { /// /// # Returns /// - /// `Some(AppConfig)` if the config was reloaded, `None`` otherwise - pub fn check_for_changes(&self) -> Option>> { - // Non-blocking check for file events - match self.rx.try_recv() { - Ok(Ok(event)) => { - // Only process write/modify events - if matches!(event.kind, EventKind::Modify(_)) { - // Add a small delay to ensure the file write is complete - thread::sleep(Duration::from_millis(100)); + /// `Some(AppConfig)` if the config was reloaded, `None` otherwise + pub fn check_for_changes(&mut self) -> Option>> { + // Process all pending events before deciding to reload + let mut should_reload = false; - // Attempt to reload the config - match load_config() { - Ok(config) => { - println!("Configuration file changed. Reloaded configuration."); - Some(Ok(config)) - } - Err(e) => { - eprintln!("Error reloading configuration: {e}"); - Some(Err(Box::new(e))) - } + loop { + match self.rx.try_recv() { + Ok(Ok(event)) => { + // Only process write/modify events + if matches!(event.kind, EventKind::Modify(_)) { + should_reload = true; + self.last_event_time = std::time::Instant::now(); } - } else { - None + } + Ok(Err(e)) => { + // File watcher error, log but continue + eprintln!("Error watching config file: {e}"); + } + Err(TryRecvError::Empty) => { + // No more events + break; + } + Err(TryRecvError::Disconnected) => { + // Channel disconnected, watcher is dead + eprintln!("Config watcher channel disconnected"); + return None; } } - // No events or channel errors - _ => None, + } + + // Debounce rapid file changes (e.g., from editors that write multiple times) + if should_reload { + // Wait to ensure file writing is complete + let debounce_time = Duration::from_millis(250); + let time_since_last_event = self.last_event_time.elapsed(); + + if time_since_last_event < debounce_time { + thread::sleep(debounce_time - time_since_last_event); + } + + // Attempt to reload the config from the specific path being watched + match load_config_from_path(Some(&self.config_path)) { + Ok(config) => Some(Ok(config)), + Err(e) => Some(Err(Box::new(e))), + } + } else { + None } } @@ -70,4 +92,4 @@ impl ConfigWatcher { pub const fn config_path(&self) -> &String { &self.config_path } -} \ No newline at end of file +} diff --git a/src/cpu.rs b/src/cpu.rs index 09290cf..ac8092c 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -104,11 +104,11 @@ pub fn set_turbo(setting: TurboSetting) -> Result<()> { TurboSetting::Never => "0", // boost = 0 means turbo is disabled TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())), }; - + // AMD specific paths let amd_pstate_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"; @@ -139,16 +139,16 @@ pub fn set_turbo(setting: TurboSetting) -> Result<()> { 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{}/cpufreq/boost", core_id); - + 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; } } - + Ok(success) } diff --git a/src/daemon.rs b/src/daemon.rs index c966af6..b31a7af 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,5 +1,5 @@ -use crate::config::{AppConfig, LogLevel}; use crate::config::watcher::ConfigWatcher; +use crate::config::{AppConfig, LogLevel}; use crate::conflict; use crate::core::SystemReport; use crate::engine; @@ -65,31 +65,45 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box = match config_file_path { - Some(path) => { - match ConfigWatcher::new(&path) { - Ok(watcher) => { - println!("Watching config file: {path}"); - Some(watcher) - }, - Err(e) => { - eprintln!("Failed to initialize config file watcher: {e}"); - None - } - } - }, - None => None, + + let mut config_watcher = if let Some(path) = config_file_path { match ConfigWatcher::new(&path) { + Ok(watcher) => { + log_message( + &effective_log_level, + LogLevel::Info, + &format!("Watching config file: {path}"), + ); + Some(watcher) + } + Err(e) => { + log_message( + &effective_log_level, + LogLevel::Warning, + &format!("Failed to initialize config file watcher: {e}"), + ); + None + } + } } else { + log_message( + &effective_log_level, + LogLevel::Warning, + "No config file found to watch for changes.", + ); + None }; // Variables for adaptive polling @@ -102,17 +116,27 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box { - if verbose { - println!("Config file changed, updating configuration"); - } + log_message( + &effective_log_level, + LogLevel::Info, + "Config file changed, updating configuration", + ); config = new_config; - }, + // Reset polling interval after config change + current_poll_interval = config.daemon.poll_interval_sec; + // Record this as a settings change for adaptive polling purposes + last_settings_change = Instant::now(); + } Err(e) => { - eprintln!("Error loading new configuration: {e}"); + log_message( + &effective_log_level, + LogLevel::Error, + &format!("Error loading new configuration: {e}"), + ); // Continue with existing config } } diff --git a/src/engine.rs b/src/engine.rs index 768a192..8611e47 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -131,7 +131,8 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() // Validate the configuration to ensure it's usable if turbo_settings.load_threshold_high <= turbo_settings.load_threshold_low { return Err(EngineError::ConfigurationError( - "Invalid turbo auto settings: high threshold must be greater than low threshold".to_string() + "Invalid turbo auto settings: high threshold must be greater than low threshold" + .to_string(), )); } diff --git a/src/monitor.rs b/src/monitor.rs index 8d1b3d1..259e872 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -414,7 +414,10 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo { None }; - let available_governors = if cpufreq_base_path.join("scaling_available_governors").exists() { + let available_governors = if cpufreq_base_path + .join("scaling_available_governors") + .exists() + { read_sysfs_file_trimmed(cpufreq_base_path.join("scaling_available_governors")).map_or_else( |_| vec![], |s| s.split_whitespace().map(String::from).collect(),