mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
commit
8e3a87cb04
11 changed files with 186 additions and 566 deletions
175
Cargo.lock
generated
175
Cargo.lock
generated
|
@ -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,21 +73,21 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
|||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
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 +194,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 +215,7 @@ dependencies = [
|
|||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -247,27 +247,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 +310,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 +350,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 +362,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 +378,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 +454,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 +500,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"
|
||||
|
@ -665,15 +545,16 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||
name = "superfreq"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"log",
|
||||
"notify",
|
||||
"num_cpus",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
@ -761,16 +642,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 +706,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 +765,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"
|
||||
|
|
|
@ -13,7 +13,8 @@ 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"
|
||||
thiserror = "2.0"
|
||||
anyhow = "1.0"
|
||||
|
|
|
@ -74,7 +74,7 @@ pub fn set_battery_charge_thresholds(start_threshold: u8, stop_threshold: u8) ->
|
|||
// Validate thresholds using `BatteryChargeThresholds`
|
||||
let thresholds =
|
||||
BatteryChargeThresholds::new(start_threshold, stop_threshold).map_err(|e| match e {
|
||||
crate::config::types::ConfigError::ValidationError(msg) => {
|
||||
crate::config::types::ConfigError::Validation(msg) => {
|
||||
ControlError::InvalidValueError(msg)
|
||||
}
|
||||
_ => ControlError::InvalidValueError(format!("Invalid battery threshold values: {e}")),
|
||||
|
|
|
@ -26,7 +26,7 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, C
|
|||
if path.exists() {
|
||||
return load_and_parse_config(path);
|
||||
}
|
||||
return Err(ConfigError::IoError(std::io::Error::new(
|
||||
return Err(ConfigError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Specified config file not found: {}", path.display()),
|
||||
)));
|
||||
|
@ -80,10 +80,9 @@ pub fn load_config_from_path(specific_path: Option<&str>) -> Result<AppConfig, C
|
|||
|
||||
/// 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 contents = fs::read_to_string(path).map_err(ConfigError::Io)?;
|
||||
|
||||
let toml_app_config =
|
||||
toml::from_str::<AppConfigToml>(&contents).map_err(ConfigError::TomlError)?;
|
||||
let toml_app_config = toml::from_str::<AppConfigToml>(&contents).map_err(ConfigError::Toml)?;
|
||||
|
||||
// Handle inheritance of values from global to profile configs
|
||||
let mut charger_profile = toml_app_config.charger.clone();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
pub mod load;
|
||||
pub mod types;
|
||||
pub mod watcher;
|
||||
|
||||
pub use load::*;
|
||||
pub use types::*;
|
||||
|
|
|
@ -23,17 +23,17 @@ pub struct BatteryChargeThresholds {
|
|||
impl BatteryChargeThresholds {
|
||||
pub fn new(start: u8, stop: u8) -> Result<Self, ConfigError> {
|
||||
if stop == 0 {
|
||||
return Err(ConfigError::ValidationError(
|
||||
return Err(ConfigError::Validation(
|
||||
"Stop threshold must be greater than 0%".to_string(),
|
||||
));
|
||||
}
|
||||
if start >= stop {
|
||||
return Err(ConfigError::ValidationError(format!(
|
||||
return Err(ConfigError::Validation(format!(
|
||||
"Start threshold ({start}) must be less than stop threshold ({stop})"
|
||||
)));
|
||||
}
|
||||
if stop > 100 {
|
||||
return Err(ConfigError::ValidationError(format!(
|
||||
return Err(ConfigError::Validation(format!(
|
||||
"Stop threshold ({stop}) cannot exceed 100%"
|
||||
)));
|
||||
}
|
||||
|
@ -98,37 +98,18 @@ pub struct AppConfig {
|
|||
}
|
||||
|
||||
// Error type for config loading
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
IoError(std::io::Error),
|
||||
TomlError(toml::de::Error),
|
||||
ValidationError(String),
|
||||
}
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
impl From<std::io::Error> for ConfigError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Self::IoError(err)
|
||||
}
|
||||
}
|
||||
#[error("TOML parsing error: {0}")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
|
||||
impl From<toml::de::Error> for ConfigError {
|
||||
fn from(err: toml::de::Error) -> Self {
|
||||
Self::TomlError(err)
|
||||
}
|
||||
#[error("Configuration validation error: {0}")]
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::IoError(e) => write!(f, "I/O error: {e}"),
|
||||
Self::TomlError(e) => write!(f, "TOML parsing error: {e}"),
|
||||
Self::ValidationError(s) => write!(f, "Configuration validation error: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
// Intermediate structs for TOML parsing
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ProfileConfigToml {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,7 +109,7 @@ pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> {
|
|||
let (is_valid, available_governors) = is_governor_valid(governor)?;
|
||||
|
||||
if !is_valid {
|
||||
return Err(ControlError::InvalidValueError(format!(
|
||||
return Err(ControlError::InvalidGovernor(format!(
|
||||
"Governor '{}' is not available on this system. Valid governors: {}",
|
||||
governor,
|
||||
available_governors.join(", ")
|
||||
|
@ -432,7 +432,7 @@ fn read_sysfs_value_as_u32(path: &str) -> Result<u32> {
|
|||
content
|
||||
.trim()
|
||||
.parse::<u32>()
|
||||
.map_err(|e| ControlError::ReadError(format!("Failed to parse value from {path}: {e}")))
|
||||
.map_err(|e| ControlError::ParseError(format!("Failed to parse value from {path}: {e}")))
|
||||
}
|
||||
|
||||
fn validate_min_frequency(core_id: u32, new_min_freq_mhz: u32) -> Result<()> {
|
||||
|
|
167
src/daemon.rs
167
src/daemon.rs
|
@ -1,9 +1,8 @@
|
|||
use crate::config::watcher::ConfigWatcher;
|
||||
use crate::config::{AppConfig, LogLevel};
|
||||
use crate::core::SystemReport;
|
||||
use crate::engine;
|
||||
use crate::monitor;
|
||||
use crate::util::error::AppError;
|
||||
use crate::util::error::{AppError, ControlError};
|
||||
use log::{LevelFilter, debug, error, info, warn};
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
|
@ -60,7 +59,15 @@ fn idle_multiplier(idle_secs: u64) -> f32 {
|
|||
}
|
||||
|
||||
/// Calculate optimal polling interval based on system conditions and history
|
||||
fn compute_new(params: &IntervalParams) -> u64 {
|
||||
///
|
||||
/// Returns Ok with the calculated interval, or Err if the configuration is invalid
|
||||
fn compute_new(
|
||||
params: &IntervalParams,
|
||||
system_history: &SystemHistory,
|
||||
) -> Result<u64, ControlError> {
|
||||
// Use the centralized validation function
|
||||
validate_poll_intervals(params.min_interval, params.max_interval)?;
|
||||
|
||||
// Start with base interval
|
||||
let mut adjusted_interval = params.base_interval;
|
||||
|
||||
|
@ -117,8 +124,31 @@ fn compute_new(params: &IntervalParams) -> u64 {
|
|||
adjusted_interval = (adjusted_interval / 2).max(1);
|
||||
}
|
||||
|
||||
// Ensure interval stays within configured bounds
|
||||
adjusted_interval.clamp(params.min_interval, params.max_interval)
|
||||
// Enforce a minimum of 1 second to prevent busy loops, regardless of params.min_interval
|
||||
let min_safe_interval = params.min_interval.max(1);
|
||||
let new_interval = adjusted_interval.clamp(min_safe_interval, params.max_interval);
|
||||
|
||||
// Blend the new interval with the cached value if available
|
||||
let blended_interval = if let Some(cached) = system_history.last_computed_interval {
|
||||
// Use a weighted average: 70% previous value, 30% new value
|
||||
// This smooths out drastic changes in polling frequency
|
||||
const PREVIOUS_VALUE_WEIGHT: u128 = 7; // 70%
|
||||
const NEW_VALUE_WEIGHT: u128 = 3; // 30%
|
||||
const TOTAL_WEIGHT: u128 = PREVIOUS_VALUE_WEIGHT + NEW_VALUE_WEIGHT; // 10
|
||||
|
||||
// XXX: Use u128 arithmetic to avoid overflow with large interval values
|
||||
let result = (u128::from(cached) * PREVIOUS_VALUE_WEIGHT
|
||||
+ u128::from(new_interval) * NEW_VALUE_WEIGHT)
|
||||
/ TOTAL_WEIGHT;
|
||||
|
||||
result as u64
|
||||
} else {
|
||||
new_interval
|
||||
};
|
||||
|
||||
// Blended result still needs to respect the configured bounds
|
||||
// Again enforce minimum of 1 second regardless of params.min_interval
|
||||
Ok(blended_interval.clamp(min_safe_interval, params.max_interval))
|
||||
}
|
||||
|
||||
/// Tracks historical system data for "advanced" adaptive polling
|
||||
|
@ -142,6 +172,8 @@ struct SystemHistory {
|
|||
last_state_change: Instant,
|
||||
/// Current system state
|
||||
current_state: SystemState,
|
||||
/// Last computed optimal polling interval
|
||||
last_computed_interval: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for SystemHistory {
|
||||
|
@ -156,6 +188,7 @@ impl Default for SystemHistory {
|
|||
state_durations: std::collections::HashMap::new(),
|
||||
last_state_change: Instant::now(),
|
||||
current_state: SystemState::default(),
|
||||
last_computed_interval: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +358,11 @@ impl SystemHistory {
|
|||
}
|
||||
|
||||
/// Calculate optimal polling interval based on system conditions
|
||||
fn calculate_optimal_interval(&self, config: &AppConfig, on_battery: bool) -> u64 {
|
||||
fn calculate_optimal_interval(
|
||||
&self,
|
||||
config: &AppConfig,
|
||||
on_battery: bool,
|
||||
) -> Result<u64, ControlError> {
|
||||
let params = IntervalParams {
|
||||
base_interval: config.daemon.poll_interval_sec,
|
||||
min_interval: config.daemon.min_poll_interval_sec,
|
||||
|
@ -338,12 +375,34 @@ impl SystemHistory {
|
|||
on_battery,
|
||||
};
|
||||
|
||||
compute_new(¶ms)
|
||||
compute_new(¶ms, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that poll interval configuration is consistent
|
||||
/// Returns Ok if configuration is valid, Err with a descriptive message if invalid
|
||||
fn validate_poll_intervals(min_interval: u64, max_interval: u64) -> Result<(), ControlError> {
|
||||
if min_interval < 1 {
|
||||
return Err(ControlError::InvalidValueError(
|
||||
"min_interval must be ≥ 1".to_string(),
|
||||
));
|
||||
}
|
||||
if max_interval < 1 {
|
||||
return Err(ControlError::InvalidValueError(
|
||||
"max_interval must be ≥ 1".to_string(),
|
||||
));
|
||||
}
|
||||
if max_interval >= min_interval {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::InvalidValueError(format!(
|
||||
"Invalid interval configuration: max_interval ({max_interval}) is less than min_interval ({min_interval})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
@ -364,6 +423,14 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
|
|||
|
||||
info!("Starting superfreq daemon...");
|
||||
|
||||
// Validate critical configuration values before proceeding
|
||||
if let Err(err) = validate_poll_intervals(
|
||||
config.daemon.min_poll_interval_sec,
|
||||
config.daemon.max_poll_interval_sec,
|
||||
) {
|
||||
return Err(AppError::Control(err));
|
||||
}
|
||||
|
||||
// Create a flag that will be set to true when a signal is received
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
|
@ -385,35 +452,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);
|
||||
|
@ -426,31 +464,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");
|
||||
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...");
|
||||
|
@ -491,16 +504,35 @@ pub fn run_daemon(mut config: AppConfig, verbose: bool) -> Result<(), AppError>
|
|||
|
||||
// Calculate optimal polling interval if adaptive polling is enabled
|
||||
if config.daemon.adaptive_interval {
|
||||
let optimal_interval =
|
||||
system_history.calculate_optimal_interval(&config, on_battery);
|
||||
match system_history.calculate_optimal_interval(&config, on_battery) {
|
||||
Ok(optimal_interval) => {
|
||||
// Store the new interval
|
||||
system_history.last_computed_interval = Some(optimal_interval);
|
||||
|
||||
debug!("Recalculated optimal interval: {optimal_interval}s");
|
||||
|
||||
// Don't change the interval too dramatically at once
|
||||
if optimal_interval > current_poll_interval {
|
||||
current_poll_interval = (current_poll_interval + optimal_interval) / 2;
|
||||
} else if optimal_interval < current_poll_interval {
|
||||
match optimal_interval.cmp(¤t_poll_interval) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
current_poll_interval =
|
||||
(current_poll_interval + optimal_interval) / 2;
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
current_poll_interval = current_poll_interval
|
||||
- ((current_poll_interval - optimal_interval) / 2).max(1);
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// No change needed when they're equal
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Log the error and stop the daemon when an invalid configuration is detected
|
||||
error!("Critical configuration error: {e}");
|
||||
running.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that we respect the (user) configured min and max limits
|
||||
current_poll_interval = current_poll_interval.clamp(
|
||||
|
@ -560,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}")?;
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ pub fn determine_and_apply_settings(
|
|||
// Let set_governor handle the validation
|
||||
if let Err(e) = cpu::set_governor(governor, None) {
|
||||
// If the governor is not available, log a warning
|
||||
if matches!(e, ControlError::InvalidValueError(_))
|
||||
if matches!(e, ControlError::InvalidGovernor(_))
|
||||
|| matches!(e, ControlError::NotSupported(_))
|
||||
{
|
||||
warn!(
|
||||
|
|
|
@ -1,194 +1,80 @@
|
|||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ControlError {
|
||||
Io(io::Error),
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Failed to write to sysfs path: {0}")]
|
||||
WriteError(String),
|
||||
|
||||
#[error("Failed to read sysfs path: {0}")]
|
||||
ReadError(String),
|
||||
|
||||
#[error("Invalid value for setting: {0}")]
|
||||
InvalidValueError(String),
|
||||
|
||||
#[error("Control action not supported: {0}")]
|
||||
NotSupported(String),
|
||||
|
||||
#[error("Permission denied: {0}. Try running with sudo.")]
|
||||
PermissionDenied(String),
|
||||
|
||||
#[error("Invalid platform control profile {0} supplied, please provide a valid one.")]
|
||||
InvalidProfile(String),
|
||||
|
||||
#[error("Invalid governor: {0}")]
|
||||
InvalidGovernor(String),
|
||||
|
||||
#[error("Failed to parse value: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Path missing: {0}")]
|
||||
PathMissing(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ControlError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
match err.kind() {
|
||||
io::ErrorKind::PermissionDenied => Self::PermissionDenied(err.to_string()),
|
||||
_ => Self::Io(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ControlError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Self::WriteError(s) => write!(f, "Failed to write to sysfs path: {s}"),
|
||||
Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"),
|
||||
Self::InvalidValueError(s) => write!(f, "Invalid value for setting: {s}"),
|
||||
Self::NotSupported(s) => write!(f, "Control action not supported: {s}"),
|
||||
Self::PermissionDenied(s) => {
|
||||
write!(f, "Permission denied: {s}. Try running with sudo.")
|
||||
}
|
||||
Self::InvalidProfile(s) => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid platform control profile {s} supplied, please provide a valid one."
|
||||
)
|
||||
}
|
||||
Self::InvalidGovernor(s) => {
|
||||
write!(f, "Invalid governor: {s}")
|
||||
}
|
||||
Self::ParseError(s) => {
|
||||
write!(f, "Failed to parse value: {s}")
|
||||
}
|
||||
Self::PathMissing(s) => {
|
||||
write!(f, "Path missing: {s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ControlError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SysMonitorError {
|
||||
Io(io::Error),
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Failed to read sysfs path: {0}")]
|
||||
ReadError(String),
|
||||
|
||||
#[error("Failed to parse value: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Failed to parse /proc/stat: {0}")]
|
||||
ProcStatParseError(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for SysMonitorError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SysMonitorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"),
|
||||
Self::ParseError(s) => write!(f, "Failed to parse value: {s}"),
|
||||
Self::ProcStatParseError(s) => {
|
||||
write!(f, "Failed to parse /proc/stat: {s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SysMonitorError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EngineError {
|
||||
ControlError(ControlError),
|
||||
#[error("CPU control error: {0}")]
|
||||
ControlError(#[from] ControlError),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
ConfigurationError(String),
|
||||
}
|
||||
|
||||
impl From<ControlError> for EngineError {
|
||||
fn from(err: ControlError) -> Self {
|
||||
Self::ControlError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EngineError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ControlError(e) => write!(f, "CPU control error: {e}"),
|
||||
Self::ConfigurationError(s) => write!(f, "Configuration error: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for EngineError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::ControlError(e) => Some(e),
|
||||
Self::ConfigurationError(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A unified error type for the entire application
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
Control(ControlError),
|
||||
Monitor(SysMonitorError),
|
||||
Engine(EngineError),
|
||||
Config(crate::config::ConfigError),
|
||||
#[error("{0}")]
|
||||
Control(#[from] ControlError),
|
||||
|
||||
#[error("{0}")]
|
||||
Monitor(#[from] SysMonitorError),
|
||||
|
||||
#[error("{0}")]
|
||||
Engine(#[from] EngineError),
|
||||
|
||||
#[error("{0}")]
|
||||
Config(#[from] crate::config::ConfigError),
|
||||
|
||||
#[error("{0}")]
|
||||
Generic(String),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl From<ControlError> for AppError {
|
||||
fn from(err: ControlError) -> Self {
|
||||
Self::Control(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SysMonitorError> for AppError {
|
||||
fn from(err: SysMonitorError) -> Self {
|
||||
Self::Monitor(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EngineError> for AppError {
|
||||
fn from(err: EngineError) -> Self {
|
||||
Self::Engine(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::config::ConfigError> for AppError {
|
||||
fn from(err: crate::config::ConfigError) -> Self {
|
||||
Self::Config(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for AppError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AppError {
|
||||
fn from(err: String) -> Self {
|
||||
Self::Generic(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AppError {
|
||||
fn from(err: &str) -> Self {
|
||||
Self::Generic(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Control(e) => write!(f, "{e}"),
|
||||
Self::Monitor(e) => write!(f, "{e}"),
|
||||
Self::Engine(e) => write!(f, "{e}"),
|
||||
Self::Config(e) => write!(f, "{e}"),
|
||||
Self::Generic(s) => write!(f, "{s}"),
|
||||
Self::Io(e) => write!(f, "I/O error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AppError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Control(e) => Some(e),
|
||||
Self::Monitor(e) => Some(e),
|
||||
Self::Engine(e) => Some(e),
|
||||
Self::Config(e) => Some(e),
|
||||
Self::Generic(_) => None,
|
||||
Self::Io(e) => Some(e),
|
||||
}
|
||||
}
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue