dynpatch_watcher/
lib.rs

1//! # dynpatch-watcher
2//!
3//! File watching and live config reloading for dynpatch.
4//!
5//! This crate provides utilities for watching files and automatically reloading
6//! configurations or patches when they change on disk.
7
8use notify::{
9    event::{Event, EventKind},
10    RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
11};
12use std::path::{Path, PathBuf};
13use thiserror::Error;
14use tracing::{debug, error, info, warn};
15
16pub mod config;
17
18pub use config::{ConfigWatcher, HotConfig};
19
20#[derive(Error, Debug)]
21pub enum WatchError {
22    #[error("Failed to watch path: {path}")]
23    WatchFailed {
24        path: PathBuf,
25        #[source]
26        source: notify::Error,
27    },
28
29    #[error("Failed to read file: {0}")]
30    ReadFailed(#[from] std::io::Error),
31
32    #[error("Failed to parse config: {0}")]
33    ParseFailed(String),
34
35    #[error("Invalid path: {0}")]
36    InvalidPath(PathBuf),
37}
38
39pub type Result<T> = std::result::Result<T, WatchError>;
40
41/// File watcher for arbitrary file changes
42pub struct FileWatcher {
43    _watcher: RecommendedWatcher,
44    path: PathBuf,
45}
46
47impl FileWatcher {
48    /// Create a new file watcher with a callback
49    pub fn new<P, F>(path: P, mut callback: F) -> Result<Self>
50    where
51        P: AsRef<Path>,
52        F: FnMut(&Path) + Send + 'static,
53    {
54        let path = path.as_ref().to_path_buf();
55        
56        if !path.exists() {
57            warn!("Watch path does not exist yet: {:?}", path);
58        }
59
60        let watch_path = path.clone();
61        let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
62            match res {
63                Ok(event) => {
64                    if matches!(
65                        event.kind,
66                        EventKind::Modify(_) | EventKind::Create(_)
67                    ) {
68                        debug!("File change detected: {:?}", event.paths);
69                        for p in &event.paths {
70                            callback(p);
71                        }
72                    }
73                }
74                Err(e) => error!("Watch error: {:?}", e),
75            }
76        })
77        .map_err(|e| WatchError::WatchFailed {
78            path: path.clone(),
79            source: e,
80        })?;
81
82        watcher
83            .watch(&watch_path, RecursiveMode::NonRecursive)
84            .map_err(|e| WatchError::WatchFailed {
85                path: path.clone(),
86                source: e,
87            })?;
88
89        info!("Watching file: {:?}", path);
90
91        Ok(Self {
92            _watcher: watcher,
93            path,
94        })
95    }
96
97    pub fn path(&self) -> &Path {
98        &self.path
99    }
100}
101
102/// Watch a file and call a callback on changes
103pub fn watch<P, F>(path: P, callback: F) -> Result<FileWatcher>
104where
105    P: AsRef<Path>,
106    F: FnMut(&Path) + Send + 'static,
107{
108    FileWatcher::new(path, callback)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::sync::atomic::{AtomicUsize, Ordering};
115    use std::sync::Arc;
116    use tempfile::NamedTempFile;
117
118    #[test]
119    fn test_watcher_creation() {
120        let temp = NamedTempFile::new().unwrap();
121        let count = Arc::new(AtomicUsize::new(0));
122        let count_clone = count.clone();
123
124        let _watcher = FileWatcher::new(temp.path(), move |_| {
125            count_clone.fetch_add(1, Ordering::SeqCst);
126        });
127
128        // Watcher should be created successfully
129        assert!(_watcher.is_ok());
130    }
131}