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:
parent
262c70fb85
commit
498d179aa8
8 changed files with 186 additions and 110 deletions
|
@ -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);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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."),
|
||||
}
|
||||
|
|
|
@ -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,33 +38,53 @@ 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
|
||||
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<Result<AppConfig, Box<dyn Error>>> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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) {
|
||||
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<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue