rust_web_server/config_watch/
mod.rs1use std::path::PathBuf;
13use std::time::{Duration, SystemTime};
14
15pub const RWS_CONFIG_WATCH: &str = "RWS_CONFIG_WATCH";
17
18pub const POLL_INTERVAL: Duration = Duration::from_secs(1);
20
21pub fn enabled() -> bool {
23 std::env::var(RWS_CONFIG_WATCH).is_ok_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
24}
25
26pub fn watched_paths() -> Vec<PathBuf> {
29 let var = |key: &str| std::env::var(key).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
30 let mut paths = vec![PathBuf::from(var("RWS_CONFIG_FILE").unwrap_or_else(|| "rws.config.toml".to_string()))];
31 for key in [crate::entry_point::Config::RWS_CONFIG_TLS_CERT_FILE, crate::entry_point::Config::RWS_CONFIG_TLS_KEY_FILE] {
32 paths.extend(var(key).map(PathBuf::from));
33 }
34 if let Some(files) = var("RWS_CONFIG_TLS_CLIENT_CRL_FILE") {
36 paths.extend(files.split(',').map(str::trim).filter(|p| !p.is_empty()).map(PathBuf::from));
37 }
38 for i in 0.. {
39 if var(&format!("RWS_CONFIG_VIRTUAL_HOST_{i}_DOMAIN")).is_none() {
40 break;
41 }
42 for suffix in ["CERT_FILE", "KEY_FILE"] {
43 paths.extend(var(&format!("RWS_CONFIG_VIRTUAL_HOST_{i}_{suffix}")).map(PathBuf::from));
44 }
45 }
46 paths.retain(|p| !p.to_string_lossy().trim_start().to_ascii_lowercase().starts_with("pkcs11:"));
48 paths.sort();
49 paths.dedup();
50 paths
51}
52
53type Snapshot = Vec<(PathBuf, Option<(u64, SystemTime)>)>;
55
56fn snapshot(paths: Vec<PathBuf>) -> Snapshot {
57 paths
58 .into_iter()
59 .map(|path| {
60 let state = std::fs::metadata(&path).ok().and_then(|md| Some((md.len(), md.modified().ok()?)));
61 (path, state)
62 })
63 .collect()
64}
65
66pub struct Watcher<F: FnMut() -> Vec<PathBuf>> {
68 paths: F,
69 applied: Snapshot,
70 pending: Option<Snapshot>,
71}
72
73impl<F: FnMut() -> Vec<PathBuf>> Watcher<F> {
74 pub fn new(mut paths: F) -> Self {
77 let applied = snapshot(paths());
78 Watcher { paths, applied, pending: None }
79 }
80
81 pub fn poll(&mut self) -> bool {
84 let now = snapshot((self.paths)());
85 if now == self.applied {
86 self.pending = None;
87 return false;
88 }
89 if self.pending.as_ref() == Some(&now) {
90 self.applied = now;
91 self.pending = None;
92 return true;
93 }
94 self.pending = Some(now);
95 false
96 }
97}
98
99pub fn start() {
102 if !enabled() {
103 return;
104 }
105 let spawned = std::thread::Builder::new().name("rws-config-watch".to_string()).spawn(|| {
106 let mut watcher = Watcher::new(watched_paths);
107 loop {
108 std::thread::sleep(POLL_INTERVAL);
109 if watcher.poll() {
110 println!("config watch: a watched file changed; reloading");
111 crate::config_reload::request_reload();
112 }
113 }
114 });
115 if let Err(e) = spawned {
116 eprintln!("config watch: could not start: {e}");
117 }
118}
119
120#[cfg(test)]
121mod tests;