fix(watcher): filter Remove events by path to prevent false removed reports (closes #15)
This commit is contained in:
@@ -35,6 +35,43 @@ struct WatchState {
|
||||
last_inode: u64,
|
||||
}
|
||||
|
||||
fn process_event(event: Event, watch_path: &Path, state: &mut WatchState) -> Option<FileEvent> {
|
||||
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<FileEvent>,
|
||||
@@ -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 {
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user