diff --git a/README.md b/README.md index 3d0436d..926ea4b 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,15 @@ has a unix socket (`/tmp/hinoirisetr.sock`) that accepts four commands - [x] добавить логгинг нормальный - [x] избавиться от env_logger и написать свой собственный - [x] сделать файл конфига а не блять компайлтайм конфиг(suckless🤢🤢🤮) -- [ ] сделать свой парсер для конфига после serde -- [ ] заебашить автоматический хотрелоад конфига -- [ ] вместо юзания notify-rust напрямую пользоваться libnotify.so и грузить её через dlopen(чтоб бинарник был маленький ваще) +- [x] сделать свой парсер для конфига после serde +- [x] заебашить автоматический хотрелоад конфига +- [x] вместо юзания notify-rust напрямую пользоваться libnotify.so и грузить её через dlopen(чтоб бинарник был маленький ваще) - [x] переползти с chrono на time потому что мне похуй не нужна суперточность(а мб и на std::time) - [ ] fix `status` command crash + +Notify: Replaced tokio::sync::Notify with smol::channel::unbounded. The Sender is used to signal, and Receiver is polled in the main loop. +UnixListener: Switched to smol::net::UnixListener. +Signal Handling: Used signal-hook with smol::spawn for async signal processing. +Timers: Replaced tokio::time::sleep with smol::Timer. +Main Loop: Used smol::future::or to combine timer and notification events, replacing tokio::select!. +Executor: Switched from #[tokio::main] to smol::block_on. diff --git a/src/lib.rs b/src/lib.rs index 13152e7..65ac39d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use std::process::Command; use time::Time; pub mod log; -pub mod time; pub mod notify; +pub mod time; #[derive(Debug, Copy, Clone)] pub struct Config { @@ -44,10 +44,29 @@ impl Default for Config { } } +#[derive(Debug)] +pub enum ConfigError { + IoError(std::io::Error), + ParseIntError(std::num::ParseIntError), + InvalidTemperature(String), + InvalidTime(String), + InvalidGamma(String), +} + +impl From for ConfigError { + fn from(err: std::io::Error) -> Self { + ConfigError::IoError(err) + } +} + +impl From for ConfigError { + fn from(err: std::num::ParseIntError) -> Self { + ConfigError::ParseIntError(err) + } +} + impl Config { - pub fn load + std::fmt::Debug>( - path: P, - ) -> Result> { + pub fn load + std::fmt::Debug>(path: P) -> Result { trace!("Config::load({path:?})"); let mut config = Self::default(); // Start with default values @@ -75,24 +94,73 @@ impl Config { match current_section.as_str() { "" => { if key_trimmed == "notification_timeout" { - config.notification_timeout = value.parse()?; + config.notification_timeout = value.parse::()?; } } "gamma" => match key_trimmed { - "day" => config.gamma_day = value.parse()?, + "day" => { + let parsed = value.parse::()?; + if parsed <= 100 { + config.gamma_day = parsed + } else { + return Err(ConfigError::InvalidGamma(value.to_string())); + } + } "night" => config.gamma_night = value.parse()?, _ => {} }, "temp" => match key_trimmed { - "day" => config.temp_day = value.parse()?, - "night" => config.temp_night = value.parse()?, + "day" => { + let parsed = value.parse::()?; + if (1000..=20000).contains(&parsed) { + config.temp_day = parsed + } else { + return Err(ConfigError::InvalidTemperature(value.to_string())); + } + } + "night" => { + let parsed = value.parse::()?; + if (1000..=20000).contains(&parsed) { + config.temp_night = parsed + } else { + return Err(ConfigError::InvalidTemperature(value.to_string())); + } + } _ => {} }, "time" => match key_trimmed { - "sunset_start" => config.sunset_start = value.parse()?, - "sunset_end" => config.sunset_end = value.parse()?, - "sunrise_start" => config.sunrise_start = value.parse()?, - "sunrise_end" => config.sunrise_end = value.parse()?, + "sunset_start" => { + let parsed = value.parse::()?; + if (0..=23).contains(&parsed) { + config.sunset_start = parsed + } else { + return Err(ConfigError::InvalidTime(value.to_string())); + } + } + "sunset_end" => { + let parsed = value.parse::()?; + if (0..=23).contains(&parsed) { + config.sunset_end = parsed + } else { + return Err(ConfigError::InvalidTime(value.to_string())); + } + } + "sunrise_start" => { + let parsed = value.parse::()?; + if (0..=23).contains(&parsed) { + config.sunrise_start = parsed + } else { + return Err(ConfigError::InvalidTime(value.to_string())); + } + } + "sunrise_end" => { + let parsed = value.parse::()?; + if (0..=23).contains(&parsed) { + config.sunrise_end = parsed + } else { + return Err(ConfigError::InvalidTime(value.to_string())); + } + } _ => {} }, _ => {} diff --git a/src/main.rs b/src/main.rs index 5bd90eb..4b9e8f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::env; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; -use std::time::Duration; +use std::time::{Duration, UNIX_EPOCH}; use hinoirisetr::notify::InitializedNotificationSystem; use hinoirisetr::time::Time; @@ -16,17 +16,49 @@ use tokio::time::sleep; const SOCKET_PATH: &str = "/tmp/hinoirisetr.sock"; static CONFIG: OnceLock>> = OnceLock::new(); +static LAST_MODIFIED: OnceLock>>> = OnceLock::new(); enum NotifyState { Enabled(InitializedNotificationSystem), Disabled, } +async fn config_realoader(notify: Arc) { + debug!("config reloader started"); + // Config polling every 5 seconds + loop { + trace!("config poll tick"); + let config_path = get_config_path(); + if config_path.exists() { + let last_modified = LAST_MODIFIED.get().expect("last_modified not init"); + let last_modified_guard = last_modified.read().await; + if let Ok(current_modified) = std::fs::metadata(&config_path) + .and_then(|m| m.modified()) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs()) + { + if let Some(last) = *last_modified_guard { + if current_modified > last { + trace!("{current_modified}"); + trace!("{last}"); + debug!("Config file modified, reloading..."); + reload_config(Arc::clone(¬ify)).await; + } + } else { + // File is new, reload to capture initial state + debug!("Config file detected, reloading..."); + reload_config(Arc::clone(¬ify)).await; + } + } + } + sleep(Duration::from_secs(5)).await; + } +} + async fn socket_server(disabled: Arc, notify: Arc) { let listener = UnixListener::bind(SOCKET_PATH).expect("Failed to bind socket"); trace!("socket server bound"); let notification: NotifyState = - match { hinoirisetr::notify::InitializedNotificationSystem::new("hinoirisetr") } { + match hinoirisetr::notify::InitializedNotificationSystem::new("hinoirisetr") { Ok(not) => NotifyState::Enabled(not), Err(_) => { info!("libnotify not found, disabling 'status_notify' command"); @@ -80,19 +112,7 @@ async fn socket_server(disabled: Arc, notify: Arc) { } "reload" => { trace!("reload dispatched"); - let config_handle = config_handle(); - let mut config = config_handle.write().await; - match Config::load(get_config_path()) { - Ok(cfg) => { - debug!("Config file found, loading..."); - *config = cfg; - notify.notify_one(); - } - Err(err) => { - error!("Failed to load config: {err}"); - warn!("Using default config."); - } - } + reload_config(notify.clone()).await; } "status_notify" => { trace!("status_notify dispatched"); @@ -156,12 +176,22 @@ async fn main() { let disabled = Arc::new(AtomicBool::new(false)); let notify = Arc::new(Notify::new()); - let cfg: Config = if get_config_path().exists() { + // load config + let config_path = get_config_path(); + let cfg: Config = if config_path.exists() { debug!("Config file found, loading..."); - match Config::load(get_config_path()) { + LAST_MODIFIED + .set(Arc::new(RwLock::new( + std::fs::metadata(&config_path) + .and_then(|m| m.modified()) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs()) + .ok(), + ))) + .unwrap(); + match Config::load(&config_path) { Ok(cfg) => cfg, Err(err) => { - error!("Failed to load config: {err}"); + error!("Failed to load config: {err:?}"); warn!("Using default config."); Config::default() } @@ -196,6 +226,14 @@ async fn main() { }); } + // Spawn config reloader + { + let notify = Arc::clone(¬ify); + tokio::spawn(async move { + config_realoader(notify).await; + }); + } + // Signal handling let mut sigint = signal(SignalKind::interrupt()).unwrap(); let mut sigterm = signal(SignalKind::terminate()).unwrap(); @@ -243,6 +281,37 @@ async fn main() { } } +// Function to handle config reloading +async fn reload_config(notify: Arc) { + trace!("reload_config called"); + let config_handle = config_handle(); + let mut config = config_handle.write().await; + let config_path = get_config_path(); + match Config::load(&config_path) { + Ok(cfg) => { + debug!("Config file reloaded successfully"); + *config = cfg; + trace!("new config: {:#?}", config); + + let last_modified = LAST_MODIFIED.get().expect("last_modified not init"); + let mut last_modified = last_modified.write().await; + tokio::task::spawn_blocking(move || { + let new_modified = std::fs::metadata(&config_path) + .and_then(|m| m.modified()) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs()) + .ok(); + trace!("last_modified: {last_modified:?}"); + trace!("new_modified: {new_modified:?}"); + *last_modified = new_modified; + }); + notify.notify_one(); + } + Err(err) => { + error!("Failed to reload config: {err:?}"); + warn!("Retaining current config"); + } + } +} fn is_binary_available(binary_name: &str) -> bool { use std::fs; if let Ok(paths) = env::var("PATH") {