use std::collections::HashMap;
use std::sync::mpsc;
use std::time::Duration;
#[must_use = "the watcher stops immediately when the handle is dropped"]
pub struct WatchHandle {
tx: mpsc::SyncSender<()>,
}
impl WatchHandle {
pub fn stop(self) {
drop(self);
}
}
impl Drop for WatchHandle {
fn drop(&mut self) {
let _ = self.tx.try_send(());
}
}
#[allow(clippy::missing_panics_doc)]
pub fn start(
keys: Vec<String>,
interval: Duration,
mut on_drift: impl FnMut(&str, &str, &str) + Send + 'static,
) -> WatchHandle {
let (tx, rx) = mpsc::sync_channel::<()>(1);
std::thread::Builder::new()
.name("lockedenv-watcher".into())
.spawn(move || {
let mut snapshot: HashMap<String, String> = keys
.iter()
.filter_map(|k| std::env::var(k).ok().map(|v| (k.clone(), v)))
.collect();
loop {
match rx.recv_timeout(interval) {
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {}
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
for key in &keys {
let current = std::env::var(key).ok();
match (snapshot.get(key.as_str()), current.as_deref()) {
(Some(old), Some(new)) if old != new => on_drift(key, old, new),
(Some(old), None) => on_drift(key, old, "<removed>"),
(None, Some(new)) => on_drift(key, "<missing>", new),
_ => {}
}
}
}));
if result.is_err() {
#[cfg(feature = "tracing")]
tracing::error!("lockedenv watcher: on_drift callback panicked");
}
for key in &keys {
match std::env::var(key).ok() {
Some(v) => {
snapshot.insert(key.clone(), v);
}
None => {
snapshot.remove(key.as_str());
}
}
}
}
})
.expect("failed to spawn lockedenv-watcher thread");
WatchHandle { tx }
}