use std::path::PathBuf;
use std::time::{Duration, SystemTime};
pub const RWS_CONFIG_WATCH: &str = "RWS_CONFIG_WATCH";
pub const POLL_INTERVAL: Duration = Duration::from_secs(1);
pub fn enabled() -> bool {
std::env::var(RWS_CONFIG_WATCH).is_ok_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
}
pub fn watched_paths() -> Vec<PathBuf> {
let var = |key: &str| std::env::var(key).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
let mut paths = vec![PathBuf::from(var("RWS_CONFIG_FILE").unwrap_or_else(|| "rws.config.toml".to_string()))];
for key in [crate::entry_point::Config::RWS_CONFIG_TLS_CERT_FILE, crate::entry_point::Config::RWS_CONFIG_TLS_KEY_FILE] {
paths.extend(var(key).map(PathBuf::from));
}
for i in 0.. {
if var(&format!("RWS_CONFIG_VIRTUAL_HOST_{i}_DOMAIN")).is_none() {
break;
}
for suffix in ["CERT_FILE", "KEY_FILE"] {
paths.extend(var(&format!("RWS_CONFIG_VIRTUAL_HOST_{i}_{suffix}")).map(PathBuf::from));
}
}
paths.sort();
paths.dedup();
paths
}
type Snapshot = Vec<(PathBuf, Option<(u64, SystemTime)>)>;
fn snapshot(paths: Vec<PathBuf>) -> Snapshot {
paths
.into_iter()
.map(|path| {
let state = std::fs::metadata(&path).ok().and_then(|md| Some((md.len(), md.modified().ok()?)));
(path, state)
})
.collect()
}
pub struct Watcher<F: FnMut() -> Vec<PathBuf>> {
paths: F,
applied: Snapshot,
pending: Option<Snapshot>,
}
impl<F: FnMut() -> Vec<PathBuf>> Watcher<F> {
pub fn new(mut paths: F) -> Self {
let applied = snapshot(paths());
Watcher { paths, applied, pending: None }
}
pub fn poll(&mut self) -> bool {
let now = snapshot((self.paths)());
if now == self.applied {
self.pending = None;
return false;
}
if self.pending.as_ref() == Some(&now) {
self.applied = now;
self.pending = None;
return true;
}
self.pending = Some(now);
false
}
}
pub fn start() {
if !enabled() {
return;
}
let spawned = std::thread::Builder::new().name("rws-config-watch".to_string()).spawn(|| {
let mut watcher = Watcher::new(watched_paths);
loop {
std::thread::sleep(POLL_INTERVAL);
if watcher.poll() {
println!("config watch: a watched file changed; reloading");
crate::config_reload::request_reload();
}
}
});
if let Err(e) = spawned {
eprintln!("config watch: could not start: {e}");
}
}
#[cfg(test)]
mod tests;