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
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<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();
// 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<AppConfig, ConfigError> {
);
}
// 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::<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);
}
println!("Loading config from: {}", path.display());
match load_and_parse_config(&path) {
Ok(config) => return Ok(config),
Err(e) => {
eprintln!("Error parsing config file {}: {}", path.display(), 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(),
})
}
/// 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;
// 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 types;
mod load;
mod types;

View file

@ -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<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
Self::IoError(err)
}
}
impl From<toml::de::Error> 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."),
}

View file

@ -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<Result<Event, notify::Error>>,
_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,35 +38,55 @@ impl ConfigWatcher {
///
/// # Returns
///
/// `Some(AppConfig)` if the config was reloaded, `None`` otherwise
pub fn check_for_changes(&self) -> Option<Result<AppConfig, Box<dyn Error>>> {
// Non-blocking check for file events
/// `Some(AppConfig)` if the config was reloaded, `None` otherwise
pub fn check_for_changes(&mut self) -> Option<Result<AppConfig, Box<dyn Error>>> {
// Process all pending events before deciding to reload
let mut should_reload = false;
loop {
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));
should_reload = true;
self.last_event_time = std::time::Instant::now();
}
}
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;
}
}
}
// 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)))
// 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
}
}
// No events or channel errors
_ => None,
}
}
/// Get the path of the config file being watched
pub const fn config_path(&self) -> &String {

View file

@ -141,7 +141,7 @@ fn try_set_per_core_boost(value: &str) -> Result<bool> {
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)?;

View file

@ -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<dyn st
}
// 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 = [
"/etc/superfreq/config.toml",
"/etc/superfreq.toml",
];
default_paths.iter()
default_paths
.iter()
.find(|&path| std::path::Path::new(path).exists())
.map(|path| (*path).to_string())
};
let config_watcher: Option<ConfigWatcher> = match config_file_path {
Some(path) => {
match ConfigWatcher::new(&path) {
let mut config_watcher = if let Some(path) = config_file_path { match ConfigWatcher::new(&path) {
Ok(watcher) => {
println!("Watching config file: {path}");
log_message(
&effective_log_level,
LogLevel::Info,
&format!("Watching config file: {path}"),
);
Some(watcher)
},
}
Err(e) => {
eprintln!("Failed to initialize config file watcher: {e}");
log_message(
&effective_log_level,
LogLevel::Warning,
&format!("Failed to initialize config file watcher: {e}"),
);
None
}
}
},
None => 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<dyn st
let start_time = Instant::now();
// 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() {
match config_result {
Ok(new_config) => {
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
}
}

View file

@ -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(),
));
}

View file

@ -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(),