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

daemon: get rid of config hot-reloading

This commit is contained in:
NotAShelf 2025-05-18 13:27:18 +03:00
parent ee5bd8f80f
commit 58273d89f4
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
5 changed files with 32 additions and 415 deletions

167
Cargo.lock generated
View file

@ -62,7 +62,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -73,7 +73,7 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -82,12 +82,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
@ -194,7 +188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -215,7 +209,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys",
]
[[package]]
@ -247,27 +241,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.2.16"
@ -331,26 +304,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -391,26 +344,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "libc"
version = "0.2.172"
@ -423,9 +356,8 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.0",
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
@ -440,58 +372,18 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.52.0",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "notify"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [
"bitflags 2.9.0",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
dependencies = [
"serde",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -556,15 +448,6 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "redox_users"
version = "0.5.0"
@ -611,15 +494,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.219"
@ -671,7 +545,6 @@ dependencies = [
"dirs",
"env_logger",
"log",
"notify",
"num_cpus",
"serde",
"toml",
@ -761,16 +634,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -835,15 +698,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "windows-core"
version = "0.61.0"
@ -903,15 +757,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

View file

@ -13,7 +13,6 @@ dirs = "6.0"
clap = { version = "4.0", features = ["derive"] }
num_cpus = "1.16"
ctrlc = "3.4"
notify = { version = "8.0.0", features = ["serde"] }
chrono = "0.4"
log = "0.4"
env_logger = "0.11"

View file

@ -1,6 +1,5 @@
pub mod load;
pub mod types;
pub mod watcher;
pub use load::*;
pub use types::*;

View file

@ -1,130 +0,0 @@
use log::{debug, error, warn};
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
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::{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 {
/// Initialize a new config watcher for the given path
pub fn new(config_path: &str) -> Result<Self, notify::Error> {
let (tx, rx) = channel();
// Create a watcher with default config
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
// Start watching the config file
watcher.watch(Path::new(config_path), RecursiveMode::NonRecursive)?;
debug!("Started watching config file: {config_path}");
Ok(Self {
rx,
_watcher: watcher,
config_path: config_path.to_string(),
last_event_time: std::time::Instant::now(),
})
}
/// Check for config file changes and reload if necessary
///
/// # Returns
///
/// `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)) => {
// Process various file events that might indicate configuration changes
match event.kind {
EventKind::Modify(_) => {
debug!("Detected modification to config file: {}", self.config_path);
should_reload = true;
}
EventKind::Create(_) => {
debug!("Detected recreation of config file: {}", self.config_path);
should_reload = true;
}
EventKind::Remove(_) => {
// Some editors delete then recreate the file when saving
// Just log this event and wait for the create event
debug!(
"Detected removal of config file: {} - waiting for recreation",
self.config_path
);
}
_ => {} // Ignore other event types
}
if should_reload {
self.last_event_time = std::time::Instant::now();
}
}
Ok(Err(e)) => {
// File watcher error, log but continue
warn!("Error watching config file: {e}");
}
Err(TryRecvError::Empty) => {
// No more events
break;
}
Err(TryRecvError::Disconnected) => {
// Channel disconnected, watcher is dead
error!("Config watcher channel disconnected");
return 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);
}
// Ensure the file exists before attempting to reload
let config_path = Path::new(&self.config_path);
if !config_path.exists() {
warn!(
"Config file does not exist after change events: {}",
self.config_path
);
return None;
}
debug!("Reloading configuration from {}", self.config_path);
// Attempt to reload the config from the specific path being watched
match load_config_from_path(Some(&self.config_path)) {
Ok(config) => {
debug!("Successfully reloaded configuration");
Some(Ok(config))
}
Err(e) => {
error!("Failed to reload configuration: {e}");
Some(Err(Box::new(e)))
}
}
} else {
None
}
}
}

View file

@ -1,4 +1,3 @@
use crate::config::watcher::ConfigWatcher;
use crate::config::{AppConfig, LogLevel};
use crate::core::SystemReport;
use crate::engine;
@ -71,14 +70,6 @@ fn compute_new(params: &IntervalParams, system_history: &SystemHistory) -> Resul
params.min_interval
);
// Return an error on invalid configuration
if !validate_poll_intervals(params.min_interval, params.max_interval) {
return Err(format!(
"Invalid interval configuration: max_interval ({}) is less than min_interval ({})",
params.max_interval, params.min_interval
));
}
// Start with base interval
let mut adjusted_interval = params.base_interval;
@ -403,7 +394,7 @@ fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> bool {
}
/// Run the daemon
pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError> {
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
@ -456,35 +447,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
info!("Stats will be written to: {stats_path}");
}
// 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 {
// Check standard config paths
let default_paths = ["/etc/xdg/superfreq/config.toml", "/etc/superfreq.toml"];
default_paths
.iter()
.find(|&path| std::path::Path::new(path).exists())
.map(|path| (*path).to_string())
};
let mut config_watcher = if let Some(path) = config_file_path {
match ConfigWatcher::new(&path) {
Ok(watcher) => {
info!("Watching config file: {path}");
Some(watcher)
}
Err(e) => {
warn!("Failed to initialize config file watcher: {e}");
None
}
}
} else {
warn!("No config file found to watch for changes.");
None
};
// 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);
@ -497,46 +459,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
while running.load(Ordering::SeqCst) {
let start_time = Instant::now();
// Check for configuration changes
if let Some(watcher) = &mut config_watcher {
if let Some(config_result) = watcher.check_for_changes() {
match config_result {
Ok(new_config) => {
info!("Config file changed, updating configuration");
// Validate min/max intervals in the new configuration
if !validate_poll_intervals(
new_config.daemon.min_poll_interval_sec,
new_config.daemon.max_poll_interval_sec,
) {
error!(
"Invalid configuration loaded: max_poll_interval_sec ({}) is less than min_poll_interval_sec ({}). Keeping previous configuration.",
new_config.daemon.max_poll_interval_sec,
new_config.daemon.min_poll_interval_sec
);
// Continue with the current configuration
continue;
}
config = new_config;
// Reset polling interval after config change
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 updated config, using 1s minimum to prevent a busy loop"
);
}
// Mark this as a system event for adaptive polling
system_history.last_user_activity = Instant::now();
}
Err(e) => {
error!("Error loading new configuration: {e}");
// Continue with existing config
}
}
}
}
match monitor::collect_system_report(&config) {
Ok(report) => {
debug!("Collected system report, applying settings...");
@ -580,12 +502,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
let current_cpu_volatility = system_history.get_cpu_volatility();
let current_idle_secs = system_history.last_user_activity.elapsed().as_secs();
// Only recalculate if there's a significant change in system metrics
// or if we haven't computed an interval yet
if system_history.last_computed_interval.is_none()
|| (system_history.last_cpu_volatility - current_cpu_volatility).abs() > 1.0
|| u64::abs_diff(system_history.last_idle_secs, current_idle_secs) > 10
{
match system_history.calculate_optimal_interval(&config, on_battery) {
Ok(optimal_interval) => {
// Store the new interval and update cached metrics
@ -593,9 +509,7 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
system_history.last_cpu_volatility = current_cpu_volatility;
system_history.last_idle_secs = current_idle_secs;
debug!(
"Recalculated optimal interval: {optimal_interval}s (significant system change detected)"
);
debug!("Recalculated optimal interval: {optimal_interval}s");
// Don't change the interval too dramatically at once
match optimal_interval.cmp(&current_poll_interval) {
@ -605,8 +519,7 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
}
std::cmp::Ordering::Less => {
current_poll_interval = current_poll_interval
- ((current_poll_interval - optimal_interval) / 2)
.max(1);
- ((current_poll_interval - optimal_interval) / 2).max(1);
}
std::cmp::Ordering::Equal => {
// No change needed when they're equal
@ -620,14 +533,6 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
break;
}
}
} else {
debug!(
"Using cached optimal interval: {}s (no significant system change)",
system_history
.last_computed_interval
.unwrap_or(current_poll_interval)
);
}
// Make sure that we respect the (user) configured min and max limits
current_poll_interval = current_poll_interval.clamp(
@ -687,7 +592,6 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er
// 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}")?;
}