ricecoder_storage/markdown_config/
watcher.rs1use crate::markdown_config::error::{MarkdownConfigError, MarkdownConfigResult};
47use crate::markdown_config::loader::ConfigurationLoader;
48use notify::{RecursiveMode, Watcher};
49use std::path::PathBuf;
50use std::sync::mpsc;
51use std::sync::Arc;
52use std::time::Duration;
53use tokio::sync::RwLock;
54use tracing::{debug, error, warn};
55
56pub struct FileWatcher {
62 loader: Arc<ConfigurationLoader>,
63 paths: Vec<PathBuf>,
64 debounce_ms: u64,
65 is_watching: Arc<RwLock<bool>>,
66}
67
68impl FileWatcher {
69 pub fn new(loader: Arc<ConfigurationLoader>, paths: Vec<PathBuf>) -> Self {
76 Self {
77 loader,
78 paths,
79 debounce_ms: 500,
80 is_watching: Arc::new(RwLock::new(false)),
81 }
82 }
83
84 pub fn with_debounce(
86 loader: Arc<ConfigurationLoader>,
87 paths: Vec<PathBuf>,
88 debounce_ms: u64,
89 ) -> Self {
90 Self {
91 loader,
92 paths,
93 debounce_ms,
94 is_watching: Arc::new(RwLock::new(false)),
95 }
96 }
97
98 pub async fn watch(&self) -> MarkdownConfigResult<()> {
103 let (tx, rx) = mpsc::channel();
105
106 let mut watcher = notify::recommended_watcher(move |res| {
108 match res {
109 Ok(event) => {
110 if let Err(e) = tx.send(event) {
111 error!("Failed to send file watch event: {}", e);
112 }
113 }
114 Err(e) => {
115 error!("File watcher error: {}", e);
116 }
117 }
118 })
119 .map_err(|e| {
120 MarkdownConfigError::watch_error(format!("Failed to create file watcher: {}", e))
121 })?;
122
123 for path in &self.paths {
125 if path.exists() {
126 watcher
127 .watch(path, RecursiveMode::Recursive)
128 .map_err(|e| {
129 MarkdownConfigError::watch_error(format!(
130 "Failed to watch path {}: {}",
131 path.display(),
132 e
133 ))
134 })?;
135 debug!("Watching configuration directory: {}", path.display());
136 }
137 }
138
139 *self.is_watching.write().await = true;
141
142 let mut last_reload = std::time::Instant::now();
144
145 loop {
146 match rx.recv_timeout(Duration::from_millis(100)) {
147 Ok(event) => {
148 if self.is_config_file_event(&event) {
150 let now = std::time::Instant::now();
151 let elapsed = now.duration_since(last_reload);
152
153 if elapsed.as_millis() as u64 >= self.debounce_ms {
155 debug!("Configuration file changed, reloading...");
156 self.reload_configurations().await;
157 last_reload = now;
158 } else {
159 debug!(
160 "Debouncing configuration reload ({}ms remaining)",
161 self.debounce_ms - elapsed.as_millis() as u64
162 );
163 }
164 }
165 }
166 Err(mpsc::RecvTimeoutError::Timeout) => {
167 continue;
169 }
170 Err(mpsc::RecvTimeoutError::Disconnected) => {
171 break;
173 }
174 }
175 }
176
177 *self.is_watching.write().await = false;
178 Ok(())
179 }
180
181 fn is_config_file_event(&self, event: ¬ify::Event) -> bool {
183 use notify::EventKind;
184
185 match event.kind {
187 EventKind::Modify(_) | EventKind::Create(_) => {}
188 _ => return false,
189 }
190
191 event.paths.iter().any(|path| {
193 if let Some(file_name) = path.file_name() {
194 if let Some(name_str) = file_name.to_str() {
195 return name_str.ends_with(".agent.md")
196 || name_str.ends_with(".mode.md")
197 || name_str.ends_with(".command.md");
198 }
199 }
200 false
201 })
202 }
203
204 async fn reload_configurations(&self) {
206 match self.loader.load_all(&self.paths).await {
207 Ok((success, errors, error_list)) => {
208 debug!(
209 "Configuration reload complete: {} successful, {} failed",
210 success, errors
211 );
212
213 if !error_list.is_empty() {
214 for (path, error) in error_list {
215 warn!(
216 "Failed to load configuration from {}: {}",
217 path.display(),
218 error
219 );
220 }
221 }
222 }
223 Err(e) => {
224 error!("Failed to reload configurations: {}", e);
225 }
226 }
227 }
228
229 pub async fn is_watching(&self) -> bool {
231 *self.is_watching.read().await
232 }
233
234 pub async fn stop(&self) {
236 *self.is_watching.write().await = false;
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::markdown_config::registry::ConfigRegistry;
244
245 #[test]
246 fn test_file_watcher_creation() {
247 let registry = Arc::new(ConfigRegistry::new());
248 let loader = Arc::new(ConfigurationLoader::new(registry));
249 let paths = vec![PathBuf::from("/tmp")];
250
251 let watcher = FileWatcher::new(loader, paths.clone());
252 assert_eq!(watcher.paths, paths);
253 assert_eq!(watcher.debounce_ms, 500);
254 }
255
256 #[test]
257 fn test_file_watcher_custom_debounce() {
258 let registry = Arc::new(ConfigRegistry::new());
259 let loader = Arc::new(ConfigurationLoader::new(registry));
260 let paths = vec![PathBuf::from("/tmp")];
261
262 let watcher = FileWatcher::with_debounce(loader, paths, 1000);
263 assert_eq!(watcher.debounce_ms, 1000);
264 }
265
266 #[test]
267 fn test_is_config_file_event() {
268 let registry = Arc::new(ConfigRegistry::new());
269 let loader = Arc::new(ConfigurationLoader::new(registry));
270 let paths = vec![PathBuf::from("/tmp")];
271 let watcher = FileWatcher::new(loader, paths);
272
273 let event = notify::Event {
275 kind: notify::EventKind::Modify(notify::event::ModifyKind::Data(
276 notify::event::DataChange::Content,
277 )),
278 paths: vec![PathBuf::from("/tmp/test.agent.md")],
279 attrs: Default::default(),
280 };
281 assert!(watcher.is_config_file_event(&event));
282
283 let event = notify::Event {
285 kind: notify::EventKind::Modify(notify::event::ModifyKind::Data(
286 notify::event::DataChange::Content,
287 )),
288 paths: vec![PathBuf::from("/tmp/test.mode.md")],
289 attrs: Default::default(),
290 };
291 assert!(watcher.is_config_file_event(&event));
292
293 let event = notify::Event {
295 kind: notify::EventKind::Modify(notify::event::ModifyKind::Data(
296 notify::event::DataChange::Content,
297 )),
298 paths: vec![PathBuf::from("/tmp/test.command.md")],
299 attrs: Default::default(),
300 };
301 assert!(watcher.is_config_file_event(&event));
302
303 let event = notify::Event {
305 kind: notify::EventKind::Modify(notify::event::ModifyKind::Data(
306 notify::event::DataChange::Content,
307 )),
308 paths: vec![PathBuf::from("/tmp/test.md")],
309 attrs: Default::default(),
310 };
311 assert!(!watcher.is_config_file_event(&event));
312
313 let event = notify::Event {
315 kind: notify::EventKind::Access(notify::event::AccessKind::Read),
316 paths: vec![PathBuf::from("/tmp/test.agent.md")],
317 attrs: Default::default(),
318 };
319 assert!(!watcher.is_config_file_event(&event));
320 }
321
322 #[tokio::test]
323 async fn test_watcher_is_watching() {
324 let registry = Arc::new(ConfigRegistry::new());
325 let loader = Arc::new(ConfigurationLoader::new(registry));
326 let paths = vec![PathBuf::from("/tmp")];
327
328 let watcher = FileWatcher::new(loader, paths);
329 assert!(!watcher.is_watching().await);
330 }
331}