use ferryman_core::{build_table, load_config, RouteTable, SharedTable};
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::Arc;
use std::time::Duration;
const DEBOUNCE: Duration = Duration::from_millis(200);
pub fn watch_config(path: &Path, table: SharedTable) -> notify::Result<RecommendedWatcher> {
let path = path.to_path_buf();
let dir = watch_dir(&path);
let file_name: Option<OsString> = path.file_name().map(|n| n.to_os_string());
let (tx, rx) = mpsc::channel::<()>();
let mut watcher: RecommendedWatcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
let event = match res {
Ok(event) => event,
Err(e) => {
tracing::warn!(error = %e, "config watch error");
let _ = tx.send(());
return;
}
};
if event
.paths
.iter()
.any(|p| p.file_name() == file_name.as_deref())
{
let _ = tx.send(());
}
})?;
watcher.watch(&dir, RecursiveMode::NonRecursive)?;
std::thread::spawn(move || {
while rx.recv().is_ok() {
loop {
match rx.recv_timeout(DEBOUNCE) {
Ok(()) => continue,
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => return,
}
}
if let Some(new_table) = reload_once(&path, &table) {
table.store(Arc::new(new_table));
table.load().publish_gauges();
tracing::info!(path = %path.display(), "config reloaded");
}
}
});
Ok(watcher)
}
fn watch_dir(config_path: &Path) -> PathBuf {
match config_path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
}
}
fn reload_once(path: &Path, table: &SharedTable) -> Option<RouteTable> {
let cfg = load_config(path)
.inspect_err(|e| {
tracing::error!(path = %path.display(), error = %format!("{e:#}"), "config reload failed; keeping old table")
})
.ok()?;
let prev = table.load_full();
build_table(cfg, Some(&prev))
.inspect_err(|e| {
tracing::error!(path = %path.display(), error = %format!("{e:#}"), "config reload failed; keeping old table")
})
.ok()
}