feat: add automatic config reloading

This commit is contained in:
Vladimir Rubin 2025-04-27 17:56:19 +03:00
parent 62ad1eebdf
commit fccd90df5c
Signed by: vavakado
GPG key ID: CAB744727F36B524
3 changed files with 177 additions and 33 deletions

View file

@ -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.

View file

@ -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<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::IoError(err)
}
}
impl From<std::num::ParseIntError> for ConfigError {
fn from(err: std::num::ParseIntError) -> Self {
ConfigError::ParseIntError(err)
}
}
impl Config {
pub fn load<P: AsRef<Path> + std::fmt::Debug>(
path: P,
) -> Result<Self, Box<dyn std::error::Error>> {
pub fn load<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, ConfigError> {
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::<u32>()?;
}
}
"gamma" => match key_trimmed {
"day" => config.gamma_day = value.parse()?,
"day" => {
let parsed = value.parse::<u16>()?;
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::<u16>()?;
if (1000..=20000).contains(&parsed) {
config.temp_day = parsed
} else {
return Err(ConfigError::InvalidTemperature(value.to_string()));
}
}
"night" => {
let parsed = value.parse::<u16>()?;
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::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunset_start = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunset_end" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunset_end = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunrise_start" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunrise_start = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
"sunrise_end" => {
let parsed = value.parse::<u8>()?;
if (0..=23).contains(&parsed) {
config.sunrise_end = parsed
} else {
return Err(ConfigError::InvalidTime(value.to_string()));
}
}
_ => {}
},
_ => {}

View file

@ -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<Arc<RwLock<Config>>> = OnceLock::new();
static LAST_MODIFIED: OnceLock<Arc<RwLock<Option<u64>>>> = OnceLock::new();
enum NotifyState {
Enabled(InitializedNotificationSystem),
Disabled,
}
async fn config_realoader(notify: Arc<Notify>) {
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(&notify)).await;
}
} else {
// File is new, reload to capture initial state
debug!("Config file detected, reloading...");
reload_config(Arc::clone(&notify)).await;
}
}
}
sleep(Duration::from_secs(5)).await;
}
}
async fn socket_server(disabled: Arc<AtomicBool>, notify: Arc<Notify>) {
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<AtomicBool>, notify: Arc<Notify>) {
}
"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(&notify);
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<Notify>) {
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") {