feat: add automatic config reloading
This commit is contained in:
parent
62ad1eebdf
commit
fccd90df5c
3 changed files with 177 additions and 33 deletions
13
README.md
13
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.
|
||||
|
|
92
src/lib.rs
92
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<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()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
|
|
105
src/main.rs
105
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<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(¬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<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(¬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<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") {
|
||||
|
|
Loading…
Reference in a new issue