use crate::Result;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
use tokio::sync::watch;
use tracing::{error, info};
pub struct YamlWatcher {
_watcher: RecommendedWatcher,
rx: mpsc::Receiver<notify::Result<Event>>,
}
impl YamlWatcher {
pub fn new(path: &Path) -> Result<(Self, watch::Receiver<()>)> {
let (tx, rx) = mpsc::channel();
let (reload_tx, reload_rx) = watch::channel(());
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<Event>| {
if let Err(e) = tx.send(res) {
error!("Failed to send file watch event: {}", e);
}
},
Config::default().with_poll_interval(Duration::from_secs(2)),
)
.map_err(|e| {
crate::YamlLdapError::Io(std::io::Error::other(format!(
"Failed to create file watcher: {}",
e
)))
})?;
let parent_dir = path.parent().unwrap_or(Path::new("."));
watcher
.watch(parent_dir, RecursiveMode::NonRecursive)
.map_err(|e| {
crate::YamlLdapError::Io(std::io::Error::other(format!(
"Failed to watch directory: {}",
e
)))
})?;
let watched_path = path.to_path_buf();
let reload_tx_clone = reload_tx.clone();
tokio::spawn(async move {
let yaml_watcher = YamlWatcher {
_watcher: watcher,
rx,
};
yaml_watcher.run(watched_path, reload_tx_clone).await;
});
Ok((
YamlWatcher {
_watcher: RecommendedWatcher::new(|_| {}, Config::default()).unwrap(),
rx: mpsc::channel().1,
},
reload_rx,
))
}
async fn run(self, watched_path: std::path::PathBuf, reload_tx: watch::Sender<()>) {
loop {
match self.rx.recv() {
Ok(Ok(event)) => {
if self.is_relevant_event(&event, &watched_path) {
info!("YAML file changed, triggering reload: {:?}", event.paths);
if let Err(e) = reload_tx.send(()) {
error!("Failed to send reload signal: {}", e);
break;
}
}
}
Ok(Err(e)) => {
error!("File watch error: {}", e);
}
Err(e) => {
error!("File watcher channel error: {}", e);
break;
}
}
}
}
fn is_relevant_event(&self, event: &Event, watched_path: &Path) -> bool {
let is_our_file = event.paths.iter().any(|p| p == watched_path);
let is_relevant_kind = matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_));
is_our_file && is_relevant_kind
}
}
#[cfg(test)]
mod tests {
use super::*;
use notify::event::{CreateKind, ModifyKind};
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_is_relevant_event_modify() {
let watcher = YamlWatcher {
_watcher: RecommendedWatcher::new(|_| {}, Config::default()).unwrap(),
rx: mpsc::channel().1,
};
let path = Path::new("/tmp/test.yaml");
let event = Event {
kind: EventKind::Modify(ModifyKind::Any),
paths: vec![path.to_path_buf()],
attrs: Default::default(),
};
assert!(watcher.is_relevant_event(&event, path));
}
#[test]
fn test_is_relevant_event_create() {
let watcher = YamlWatcher {
_watcher: RecommendedWatcher::new(|_| {}, Config::default()).unwrap(),
rx: mpsc::channel().1,
};
let path = Path::new("/tmp/test.yaml");
let event = Event {
kind: EventKind::Create(CreateKind::Any),
paths: vec![path.to_path_buf()],
attrs: Default::default(),
};
assert!(watcher.is_relevant_event(&event, path));
}
#[test]
fn test_is_relevant_event_wrong_file() {
let watcher = YamlWatcher {
_watcher: RecommendedWatcher::new(|_| {}, Config::default()).unwrap(),
rx: mpsc::channel().1,
};
let watched_path = Path::new("/tmp/test.yaml");
let other_path = Path::new("/tmp/other.yaml");
let event = Event {
kind: EventKind::Modify(ModifyKind::Any),
paths: vec![other_path.to_path_buf()],
attrs: Default::default(),
};
assert!(!watcher.is_relevant_event(&event, watched_path));
}
#[test]
fn test_is_relevant_event_wrong_kind() {
let watcher = YamlWatcher {
_watcher: RecommendedWatcher::new(|_| {}, Config::default()).unwrap(),
rx: mpsc::channel().1,
};
let path = Path::new("/tmp/test.yaml");
let event = Event {
kind: EventKind::Access(notify::event::AccessKind::Any),
paths: vec![path.to_path_buf()],
attrs: Default::default(),
};
assert!(!watcher.is_relevant_event(&event, path));
}
#[tokio::test]
async fn test_yaml_watcher_new() {
let temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file.as_file(), "test: data").unwrap();
let result = YamlWatcher::new(temp_file.path());
assert!(result.is_ok());
let (_watcher, rx) = result.unwrap();
assert!(rx.has_changed().is_ok());
}
#[allow(dead_code)]
async fn test_yaml_watcher_file_change() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file.as_file(), "test: data").unwrap();
temp_file.flush().unwrap();
let (_watcher, mut rx) = YamlWatcher::new(temp_file.path()).unwrap();
let _ = rx.changed().await;
writeln!(temp_file.as_file(), "test: modified").unwrap();
temp_file.flush().unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
let _ = rx.has_changed();
}
#[test]
fn test_yaml_watcher_invalid_path() {
let result = YamlWatcher::new(Path::new("/nonexistent/parent/file.yaml"));
assert!(result.is_err());
}
}