1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

config: improve watcher; debounce

This commit is contained in:
NotAShelf 2025-05-14 01:38:55 +03:00
parent 262c70fb85
commit 498d179aa8
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
8 changed files with 186 additions and 110 deletions

View file

@ -1,18 +1,43 @@
// Configuration loading functionality // Configuration loading functionality
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig}; use crate::config::types::{AppConfig, AppConfigToml, ConfigError, DaemonConfig, ProfileConfig};
// The primary function to load application configuration. /// The primary function to load application configuration from a specific path or from default locations.
// It tries user-specific and then system-wide TOML files. ///
// Falls back to default settings if no file is found or if parsing fails. /// # 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<AppConfig, ConfigError> { pub fn load_config() -> Result<AppConfig, ConfigError> {
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<AppConfig, ConfigError> {
// 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<PathBuf> = Vec::new(); let mut config_paths: Vec<PathBuf> = Vec::new();
// User-specific path // User-specific path
if let Some(home_dir) = dirs::home_dir() { 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); config_paths.push(user_config_path);
} else { } else {
eprintln!( eprintln!(
@ -20,43 +45,18 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
); );
} }
// System-wide path // System-wide paths
let system_config_path = PathBuf::from("/etc/auto_cpufreq_rs/config.toml"); config_paths.push(PathBuf::from("/etc/superfreq/config.toml"));
config_paths.push(system_config_path); config_paths.push(PathBuf::from("/etc/superfreq.toml"));
for path in config_paths { for path in config_paths {
if path.exists() { if path.exists() {
println!("Attempting to load config from: {}", path.display()); println!("Loading config from: {}", path.display());
match fs::read_to_string(&path) { match load_and_parse_config(&path) {
Ok(contents) => { Ok(config) => return Ok(config),
match toml::from_str::<AppConfigToml>(&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);
}
}
}
Err(e) => { Err(e) => {
eprintln!("Error reading config file {}: {}", path.display(), e); eprintln!("Error with config file {}: {}", path.display(), e);
// Continue trying other files
} }
} }
} }
@ -74,3 +74,29 @@ pub fn load_config() -> Result<AppConfig, ConfigError> {
daemon: DaemonConfig::default(), daemon: DaemonConfig::default(),
}) })
} }
/// Load and parse a configuration file
fn load_and_parse_config(path: &Path) -> Result<AppConfig, ConfigError> {
let contents = fs::read_to_string(path).map_err(ConfigError::IoError)?;
let toml_app_config =
toml::from_str::<AppConfigToml>(&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,
},
})
}

View file

@ -1,9 +1,9 @@
pub mod watcher; pub mod watcher;
// Re-export all configuration types and functions // Re-export all configuration types and functions
pub use self::types::*;
pub use self::load::*; pub use self::load::*;
pub use self::types::*;
// Internal organization of config submodules // Internal organization of config submodules
mod types;
mod load; mod load;
mod types;

View file

@ -51,29 +51,29 @@ const fn default_poll_interval_sec() -> u64 {
// Error type for config loading // Error type for config loading
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigError { pub enum ConfigError {
Io(std::io::Error), IoError(std::io::Error),
Toml(toml::de::Error), TomlError(toml::de::Error),
NoValidConfigFound, NoValidConfigFound,
HomeDirNotFound, HomeDirNotFound,
} }
impl From<std::io::Error> for ConfigError { impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self { fn from(err: std::io::Error) -> Self {
Self::Io(err) Self::IoError(err)
} }
} }
impl From<toml::de::Error> for ConfigError { impl From<toml::de::Error> for ConfigError {
fn from(err: toml::de::Error) -> Self { fn from(err: toml::de::Error) -> Self {
Self::Toml(err) Self::TomlError(err)
} }
} }
impl std::fmt::Display for ConfigError { impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Io(e) => write!(f, "I/O error: {e}"), Self::IoError(e) => write!(f, "I/O error: {e}"),
Self::Toml(e) => write!(f, "TOML parsing error: {e}"), Self::TomlError(e) => write!(f, "TOML parsing error: {e}"),
Self::NoValidConfigFound => write!(f, "No valid configuration file found."), Self::NoValidConfigFound => write!(f, "No valid configuration file found."),
Self::HomeDirNotFound => write!(f, "Could not determine user home directory."), Self::HomeDirNotFound => write!(f, "Could not determine user home directory."),
} }

View file

@ -1,17 +1,18 @@
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 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::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 /// Watches a configuration file for changes and reloads it when modified
pub struct ConfigWatcher { pub struct ConfigWatcher {
rx: Receiver<Result<Event, notify::Error>>, rx: Receiver<Result<Event, notify::Error>>,
_watcher: RecommendedWatcher, // keep watcher alive while watching _watcher: RecommendedWatcher, // keep watcher alive while watching
config_path: String, config_path: String,
last_event_time: std::time::Instant,
} }
impl ConfigWatcher { impl ConfigWatcher {
@ -29,6 +30,7 @@ impl ConfigWatcher {
rx, rx,
_watcher: watcher, _watcher: watcher,
config_path: config_path.to_string(), config_path: config_path.to_string(),
last_event_time: std::time::Instant::now(),
}) })
} }
@ -36,33 +38,53 @@ impl ConfigWatcher {
/// ///
/// # Returns /// # Returns
/// ///
/// `Some(AppConfig)` if the config was reloaded, `None`` otherwise /// `Some(AppConfig)` if the config was reloaded, `None` otherwise
pub fn check_for_changes(&self) -> Option<Result<AppConfig, Box<dyn Error>>> { pub fn check_for_changes(&mut self) -> Option<Result<AppConfig, Box<dyn Error>>> {
// Non-blocking check for file events // Process all pending events before deciding to reload
match self.rx.try_recv() { let mut should_reload = false;
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));
// Attempt to reload the config loop {
match load_config() { match self.rx.try_recv() {
Ok(config) => { Ok(Ok(event)) => {
println!("Configuration file changed. Reloaded configuration."); // Only process write/modify events
Some(Ok(config)) if matches!(event.kind, EventKind::Modify(_)) {
} should_reload = true;
Err(e) => { self.last_event_time = std::time::Instant::now();
eprintln!("Error reloading configuration: {e}");
Some(Err(Box::new(e)))
}
} }
} 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
} }
} }

View file

@ -141,7 +141,7 @@ fn try_set_per_core_boost(value: &str) -> Result<bool> {
let num_cores = get_logical_core_count()?; let num_cores = get_logical_core_count()?;
for core_id in 0..num_cores { 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() { if Path::new(&boost_path).exists() {
write_sysfs_value(&boost_path, value)?; write_sysfs_value(&boost_path, value)?;

View file

@ -1,5 +1,5 @@
use crate::config::{AppConfig, LogLevel};
use crate::config::watcher::ConfigWatcher; use crate::config::watcher::ConfigWatcher;
use crate::config::{AppConfig, LogLevel};
use crate::conflict; use crate::conflict;
use crate::core::SystemReport; use crate::core::SystemReport;
use crate::engine; use crate::engine;
@ -65,31 +65,45 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
} }
// Initialize config file watcher if a path is available // Initialize config file watcher if a path is available
let config_file_path = if let Ok(path) = std::env::var("SUPERFREQ_CONFIG") { Some(path) } else { let config_file_path = if let Ok(path) = std::env::var("SUPERFREQ_CONFIG") {
Some(path)
} else {
// Check standard config paths
let default_paths = [ let default_paths = [
"/etc/superfreq/config.toml", "/etc/superfreq/config.toml",
"/etc/superfreq.toml", "/etc/superfreq.toml",
]; ];
default_paths.iter() default_paths
.iter()
.find(|&path| std::path::Path::new(path).exists()) .find(|&path| std::path::Path::new(path).exists())
.map(|path| (*path).to_string()) .map(|path| (*path).to_string())
}; };
let config_watcher: Option<ConfigWatcher> = match config_file_path { let mut config_watcher = if let Some(path) = config_file_path { match ConfigWatcher::new(&path) {
Some(path) => { Ok(watcher) => {
match ConfigWatcher::new(&path) { log_message(
Ok(watcher) => { &effective_log_level,
println!("Watching config file: {path}"); LogLevel::Info,
Some(watcher) &format!("Watching config file: {path}"),
}, );
Err(e) => { Some(watcher)
eprintln!("Failed to initialize config file watcher: {e}"); }
None Err(e) => {
} log_message(
} &effective_log_level,
}, LogLevel::Warning,
None => None, &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 // Variables for adaptive polling
@ -102,17 +116,27 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), Box<dyn st
let start_time = Instant::now(); let start_time = Instant::now();
// Check for configuration changes // Check for configuration changes
if let Some(watcher) = &config_watcher { if let Some(watcher) = &mut config_watcher {
if let Some(config_result) = watcher.check_for_changes() { if let Some(config_result) = watcher.check_for_changes() {
match config_result { match config_result {
Ok(new_config) => { Ok(new_config) => {
if verbose { log_message(
println!("Config file changed, updating configuration"); &effective_log_level,
} LogLevel::Info,
"Config file changed, updating configuration",
);
config = new_config; 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) => { 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 // Continue with existing config
} }
} }

View file

@ -131,7 +131,8 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<()
// Validate the configuration to ensure it's usable // Validate the configuration to ensure it's usable
if turbo_settings.load_threshold_high <= turbo_settings.load_threshold_low { if turbo_settings.load_threshold_high <= turbo_settings.load_threshold_low {
return Err(EngineError::ConfigurationError( 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(),
)); ));
} }

View file

@ -414,7 +414,10 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
None 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( read_sysfs_file_trimmed(cpufreq_base_path.join("scaling_available_governors")).map_or_else(
|_| vec![], |_| vec![],
|s| s.split_whitespace().map(String::from).collect(), |s| s.split_whitespace().map(String::from).collect(),