From 420b853cb9936bf502a8e72f4a030a9492cb6f8f Mon Sep 17 00:00:00 2001 From: dailz Date: Tue, 9 Jun 2026 13:18:23 +0800 Subject: [PATCH] fix(watcher): filter Remove events by path to prevent false removed reports (closes #15) --- crates/core/src/watcher/file_watcher.rs | 126 +++++++++++++++++------- 1 file changed, 90 insertions(+), 36 deletions(-) diff --git a/crates/core/src/watcher/file_watcher.rs b/crates/core/src/watcher/file_watcher.rs index 1d03abd..c068dfb 100644 --- a/crates/core/src/watcher/file_watcher.rs +++ b/crates/core/src/watcher/file_watcher.rs @@ -35,6 +35,43 @@ struct WatchState { last_inode: u64, } +fn process_event(event: Event, watch_path: &Path, state: &mut WatchState) -> Option { + if !event.paths.iter().any(|p| p == watch_path) { + return None; + } + + match event.kind { + EventKind::Modify(_) | EventKind::Create(_) | EventKind::Any => {} + EventKind::Remove(_) => return Some(FileEvent::Removed), + _ => return None, + } + + let current_inode = get_inode(watch_path).unwrap_or(0); + let current_size = std::fs::metadata(watch_path).map(|m| m.len()).unwrap_or(0); + + if current_inode != 0 && state.last_inode != 0 && current_inode != state.last_inode { + state.last_inode = current_inode; + state.last_size = current_size; + Some(FileEvent::Rotated { + new_inode: current_inode, + }) + } else if current_size > state.last_size { + state.last_size = current_size; + state.last_inode = current_inode; + Some(FileEvent::Appended { + new_size: current_size, + }) + } else if current_size < state.last_size { + state.last_size = current_size; + state.last_inode = current_inode; + Some(FileEvent::Truncated { + new_size: current_size, + }) + } else { + None + } +} + // ─── FileWatcher ──────────────────────────────────────────────────────────── pub struct FileWatcher { rx: Receiver, @@ -65,47 +102,14 @@ impl FileWatcher { } }; - match event.kind { - EventKind::Modify(_) | EventKind::Create(_) | EventKind::Any => {} - EventKind::Remove(_) => { - let _ = tx.try_send(FileEvent::Removed); - return; - } - _ => return, - } - - if !event.paths.iter().any(|p| p == &watch_path) { - return; - } - - let current_inode = get_inode(&watch_path).unwrap_or(0); - let current_size = std::fs::metadata(&watch_path).map(|m| m.len()).unwrap_or(0); - let mut st = state.lock().unwrap_or_else(|poison| { // Recover from poisoned mutex — state only tracks last_size // and last_inode for event dedup. Stale values at worst // cause a duplicate event, which is harmless. poison.into_inner() }); - - if current_inode != 0 && st.last_inode != 0 && current_inode != st.last_inode { - let _ = tx.try_send(FileEvent::Rotated { - new_inode: current_inode, - }); - st.last_inode = current_inode; - st.last_size = current_size; - } else if current_size > st.last_size { - let _ = tx.try_send(FileEvent::Appended { - new_size: current_size, - }); - st.last_size = current_size; - st.last_inode = current_inode; - } else if current_size < st.last_size { - let _ = tx.try_send(FileEvent::Truncated { - new_size: current_size, - }); - st.last_size = current_size; - st.last_inode = current_inode; + if let Some(fe) = process_event(event, &watch_path, &mut st) { + let _ = tx.try_send(fe); } })?; @@ -150,6 +154,56 @@ mod tests { events } + #[test] + fn test_remove_wrong_path_ignored() { + let dir = tempfile::tempdir().expect("create temp dir"); + let watched = dir.path().join("watched.log"); + let other = dir.path().join("other.log"); + std::fs::write(&watched, b"hello\n").expect("write watched"); + std::fs::write(&other, b"other\n").expect("write other"); + + let mut state = WatchState { + last_size: 6, + last_inode: get_inode(&watched).unwrap_or(0), + }; + + let event = Event { + kind: EventKind::Remove(notify::event::RemoveKind::File), + paths: vec![other.clone()], + attrs: Default::default(), + }; + + let result = process_event(event, &watched, &mut state); + assert_eq!( + result, None, + "Remove for non-watched path should be ignored" + ); + } + + #[test] + fn test_remove_correct_path_emits_removed() { + let dir = tempfile::tempdir().expect("create temp dir"); + let watched = dir.path().join("watched.log"); + std::fs::write(&watched, b"hello\n").expect("write watched"); + + let mut state = WatchState { + last_size: 6, + last_inode: get_inode(&watched).unwrap_or(0), + }; + + let event = Event { + kind: EventKind::Remove(notify::event::RemoveKind::File), + paths: vec![watched.clone()], + attrs: Default::default(), + }; + + let result = process_event(event, &watched, &mut state); + assert!( + matches!(result, Some(FileEvent::Removed)), + "Remove for watched path should emit Removed" + ); + } + #[test] fn test_watcher_append() { let dir = tempfile::tempdir().expect("create temp dir"); @@ -253,4 +307,4 @@ mod tests { } ); } -} \ No newline at end of file +}