1use std::collections::{HashMap, HashSet};
142use std::ffi::CString;
143use std::fs;
144use std::io::{BufRead, BufReader, Result};
145use std::os::unix::ffi::OsStrExt;
146use std::os::unix::io::AsRawFd;
147use std::path::{Path, PathBuf};
148use std::time::Duration;
149use tokio::io::unix::AsyncFd;
150
151#[derive(Debug, Clone)]
156struct GlobPattern {
157 pattern: String,
158}
159
160impl PartialEq for GlobPattern {
161 fn eq(&self, other: &Self) -> bool {
162 self.pattern == other.pattern
163 }
164}
165
166impl GlobPattern {
167 fn new(pattern: &str) -> Self {
168 Self {
169 pattern: pattern.to_string(),
170 }
171 }
172
173 fn matches(&self, text: &str) -> bool {
174 Self::match_recursive(self.pattern.as_bytes(), text.as_bytes())
175 }
176
177 fn match_recursive(pattern: &[u8], text: &[u8]) -> bool {
178 let mut p = 0;
179 let mut t = 0;
180
181 let mut star_p = None;
183 let mut star_t = None;
184
185 while t < text.len() {
186 if p < pattern.len() {
187 match pattern[p] {
188 b'*' => {
189 star_p = Some(p);
191 star_t = Some(t);
192 p += 1;
193 continue;
194 }
195 b'?' => {
196 p += 1;
198 t += 1;
199 continue;
200 }
201 b'[' => {
202 if let Some((matched, end_pos)) =
204 Self::match_char_class(&pattern[p..], text[t])
205 {
206 if matched {
207 p += end_pos;
208 t += 1;
209 continue;
210 }
211 }
212 }
214 c => {
215 if c == text[t] {
217 p += 1;
218 t += 1;
219 continue;
220 }
221 }
223 }
224 }
225
226 if let (Some(sp), Some(st)) = (star_p, star_t) {
228 p = sp + 1;
230 star_t = Some(st + 1);
231 t = st + 1;
232 } else {
233 return false;
234 }
235 }
236
237 while p < pattern.len() && pattern[p] == b'*' {
239 p += 1;
240 }
241
242 p == pattern.len()
243 }
244
245 fn match_char_class(pattern: &[u8], ch: u8) -> Option<(bool, usize)> {
248 if pattern.is_empty() || pattern[0] != b'[' {
249 return None;
250 }
251
252 let mut i = 1;
253 let mut matched = false;
254 let negated = i < pattern.len() && (pattern[i] == b'!' || pattern[i] == b'^');
255 if negated {
256 i += 1;
257 }
258
259 while i < pattern.len() {
260 if pattern[i] == b']' && i > 1 + (negated as usize) {
261 return Some((matched != negated, i + 1));
263 }
264
265 if i + 2 < pattern.len() && pattern[i + 1] == b'-' && pattern[i + 2] != b']' {
267 let start = pattern[i];
268 let end = pattern[i + 2];
269 if ch >= start && ch <= end {
270 matched = true;
271 }
272 i += 3;
273 } else {
274 if pattern[i] == ch {
276 matched = true;
277 }
278 i += 1;
279 }
280 }
281
282 None
284 }
285}
286
287#[derive(Debug, Clone, PartialEq)]
288enum Segment {
289 Exact(String),
290 Wildcard(GlobPattern),
291 DoubleWildcard, }
293
294#[derive(Debug, Clone)]
295struct Pattern {
296 segments: Vec<Segment>,
297}
298
299impl Pattern {
300 fn parse(pattern: &str) -> Self {
301 let mut segments = Vec::new();
302
303 let effective_pattern = if !pattern.contains('/') {
305 format!("**/{}", pattern)
306 } else {
307 pattern.trim_start_matches('/').to_string()
308 };
309
310 let normalized = effective_pattern.replace("//", "/");
311
312 for part in normalized.split('/') {
313 if part.is_empty() || part == "." {
314 continue;
315 }
316
317 if part == "**" {
318 segments.push(Segment::DoubleWildcard);
319 } else if part.contains('*') || part.contains('?') || part.contains('[') {
320 segments.push(Segment::Wildcard(GlobPattern::new(part)));
321 } else {
322 segments.push(Segment::Exact(part.to_string()));
323 }
324 }
325
326 Pattern { segments }
327 }
328
329 fn check(&self, path_segments: &[String], allow_prefix: bool) -> bool {
330 let pattern_segments = &self.segments;
331 let mut path_index = 0;
332
333 for pattern_index in 0..pattern_segments.len() {
334 let pattern_segment = &pattern_segments[pattern_index];
335
336 if path_index >= path_segments.len() {
337 if pattern_segment == &Segment::DoubleWildcard && pattern_index == pattern_segments.len() - 1
339 {
340 return true;
342 }
343 return allow_prefix;
345 }
346
347 match &pattern_segment {
348 Segment::Exact(s) => {
349 if s != &path_segments[path_index] {
350 return false;
351 }
352 path_index += 1;
353 }
354 Segment::Wildcard(p) => {
355 if !p.matches(&path_segments[path_index]) {
356 return false;
357 }
358 path_index += 1;
359 }
360 Segment::DoubleWildcard => {
361 if allow_prefix {
362 return true;
365 }
366
367 let patterns_left = pattern_segments.len() - (pattern_index + 1);
368 let next_path_index = path_segments.len() - patterns_left;
369 if next_path_index < path_index {
370 return false;
371 }
372 path_index = next_path_index;
373 }
374 }
375 }
376
377 if path_index < path_segments.len() {
379 return false;
380 }
381
382 !allow_prefix
385 }
386}
387
388struct Inotify {
391 fd: AsyncFd<i32>,
392}
393
394impl Inotify {
395 fn new() -> Result<Self> {
396 let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
397 if fd < 0 {
398 return Err(std::io::Error::last_os_error());
399 }
400 Ok(Self {
401 fd: AsyncFd::new(fd)?,
402 })
403 }
404
405 fn add_watch(&self, path: &Path, mask: u32) -> Result<i32> {
406 let c_path = CString::new(path.as_os_str().as_bytes())
407 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
408 let wd = unsafe { libc::inotify_add_watch(self.fd.as_raw_fd(), c_path.as_ptr(), mask) };
409 if wd < 0 {
410 return Err(std::io::Error::last_os_error());
411 }
412 Ok(wd)
413 }
414
415 async fn read_events(&self, buffer: &mut [u8]) -> Result<usize> {
416 loop {
417 let mut guard = self.fd.readable().await?;
418 match guard.try_io(|inner| {
419 let res = unsafe {
420 libc::read(
421 inner.as_raw_fd(),
422 buffer.as_mut_ptr() as *mut _,
423 buffer.len(),
424 )
425 };
426 if res < 0 {
427 Err(std::io::Error::last_os_error())
428 } else {
429 Ok(res as usize)
430 }
431 }) {
432 Ok(Ok(len)) => return Ok(len),
433 Ok(Err(e)) => {
434 if e.kind() == std::io::ErrorKind::WouldBlock {
435 continue;
436 }
437 return Err(e);
438 }
439 Err(_) => continue,
440 }
441 }
442 }
443}
444
445impl Drop for Inotify {
446 fn drop(&mut self) {
447 unsafe { libc::close(self.fd.as_raw_fd()) };
448 }
449}
450
451fn path_to_segments(path: &Path) -> Vec<String> {
454 let path_str = path.to_string_lossy();
455 let path_str = path_str.replace("//", "/");
456 path_str
457 .split('/')
458 .filter(|s| !s.is_empty())
459 .map(|s| s.to_string())
460 .collect()
461}
462
463const INOTIFY_MASK: u32 = libc::IN_MODIFY
464 | libc::IN_CLOSE_WRITE
465 | libc::IN_CREATE
466 | libc::IN_DELETE
467 | libc::IN_MOVED_FROM
468 | libc::IN_MOVED_TO
469 | libc::IN_DONT_FOLLOW;
470
471
472fn parse_inotify_events(buffer: &[u8], len: usize) -> Vec<(i32, u32, String)> {
473 let mut events = Vec::new();
474 let mut ptr = buffer.as_ptr();
475 let end = unsafe { ptr.add(len) };
476
477 while ptr < end {
478 let event = unsafe { &*(ptr as *const libc::inotify_event) };
479 let name_len = event.len as usize;
480
481 if name_len > 0 {
482 let name_ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>()) };
483 let name_slice =
484 unsafe { std::slice::from_raw_parts(name_ptr, name_len) };
485 let name_str = String::from_utf8_lossy(name_slice)
486 .trim_matches(char::from(0))
487 .to_string();
488 events.push((event.wd, event.mask, name_str));
489 }
490
491 ptr = unsafe { ptr.add(std::mem::size_of::<libc::inotify_event>() + name_len) };
492 }
493
494 events
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum WatchEvent {
503 Create,
507 Delete,
511 Update,
517 Initial,
523 DebugWatch,
528}
529
530#[derive(Debug, Clone)]
551pub struct Matcher {
552 includes: Vec<Pattern>,
553 excludes: Vec<Pattern>,
554}
555
556impl Matcher {
557 pub fn new<I: AsRef<str>, E: AsRef<str>>(includes: &[I], excludes: &[E]) -> Self {
561 Matcher {
562 includes: includes.iter().map(|p| Pattern::parse(p.as_ref())).collect(),
563 excludes: excludes.iter().map(|p| Pattern::parse(p.as_ref())).collect(),
564 }
565 }
566
567 pub fn matches(&self, path: impl AsRef<Path>) -> bool {
572 let segments = path_to_segments(path.as_ref());
573 let any_match = |patterns: &[Pattern]| {
574 (1..=segments.len())
575 .any(|end| patterns.iter().any(|p| p.check(&segments[..end], false)))
576 };
577 !any_match(&self.excludes) && any_match(&self.includes)
578 }
579}
580
581pub struct Watcher {
604 includes: Vec<String>,
605 excludes: Vec<String>,
606 base_dir: PathBuf,
607 watch_create: bool,
608 watch_delete: bool,
609 watch_update: bool,
610 watch_initial: bool,
611 match_files: bool,
612 match_dirs: bool,
613 return_absolute: bool,
614 debug_watches_enabled: bool,
615}
616
617#[deprecated(since = "0.1.2", note = "Renamed to Watcher")]
619pub type WatchBuilder = Watcher;
620
621impl Default for Watcher {
622 fn default() -> Self {
623 Self::new()
624 }
625}
626
627struct WatcherState<F> {
629 root: PathBuf,
630 inotify: Inotify,
631 watches: HashMap<i32, PathBuf>,
632 paths: HashSet<PathBuf>,
633 include_patterns: Vec<Pattern>,
634 exclude_patterns: Vec<Pattern>,
635 callback: F,
636}
637
638impl Watcher {
639 pub fn new() -> Self {
649 Watcher {
650 includes: Vec::new(),
651 excludes: Vec::new(),
652 base_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
653 watch_create: true,
654 watch_delete: true,
655 watch_update: true,
656 watch_initial: false,
657 match_files: true,
658 match_dirs: true,
659 return_absolute: false,
660 debug_watches_enabled: false,
661 }
662 }
663
664 pub fn debug_watches(mut self, enabled: bool) -> Self {
669 self.debug_watches_enabled = enabled;
670 self
671 }
672
673 pub fn add_include(mut self, pattern: impl Into<String>) -> Self {
684 self.includes.push(pattern.into());
685 self
686 }
687
688 pub fn add_includes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
690 self.includes.extend(patterns.into_iter().map(|p| p.into()));
691 self
692 }
693
694 pub fn add_exclude(mut self, pattern: impl Into<String>) -> Self {
698 self.excludes.push(pattern.into());
699 self
700 }
701
702 pub fn add_excludes(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
704 self.excludes.extend(patterns.into_iter().map(|p| p.into()));
705 self
706 }
707
708 pub fn add_ignore_file(mut self, path: impl AsRef<Path>) -> Self {
735 let path = path.as_ref();
736
737 let full_path = if path.is_absolute() {
739 path.to_path_buf()
740 } else {
741 self.base_dir.join(path)
742 };
743
744 if let Ok(file) = fs::File::open(&full_path) {
745 let reader = BufReader::new(file);
746 let mut has_negation = false;
747 for line in reader.lines().map_while(Result::ok) {
748 let trimmed = line.trim();
749
750 if trimmed.is_empty() || trimmed.starts_with('#') {
752 continue;
753 }
754
755 if trimmed.starts_with('!') {
757 has_negation = true;
758 } else {
759 self.excludes.push(trimmed.to_string());
761 }
762 }
763 if has_negation {
764 println!("Warning: negation patterns (!) in {} are ignored; excludes always take precedence over includes in this library", full_path.display());
765 }
766 }
767
768 self
769 }
770
771 pub fn set_base_dir(mut self, base_dir: impl Into<PathBuf>) -> Self {
776 self.base_dir = base_dir.into();
777 self
778 }
779
780 pub fn watch_create(mut self, enabled: bool) -> Self {
784 self.watch_create = enabled;
785 self
786 }
787
788 pub fn watch_delete(mut self, enabled: bool) -> Self {
792 self.watch_delete = enabled;
793 self
794 }
795
796 pub fn watch_update(mut self, enabled: bool) -> Self {
800 self.watch_update = enabled;
801 self
802 }
803
804 pub fn watch_initial(mut self, enabled: bool) -> Self {
830 self.watch_initial = enabled;
831 self
832 }
833
834 pub fn match_files(mut self, enabled: bool) -> Self {
838 self.match_files = enabled;
839 self
840 }
841
842 pub fn match_dirs(mut self, enabled: bool) -> Self {
846 self.match_dirs = enabled;
847 self
848 }
849
850 pub fn return_absolute(mut self, enabled: bool) -> Self {
855 self.return_absolute = enabled;
856 self
857 }
858
859 pub async fn run<F>(self, callback: F) -> Result<()>
867 where
868 F: FnMut(WatchEvent, PathBuf),
869 {
870 self.run_internal(callback, None).await
871 }
872
873 pub async fn run_debounced<F>(self, ms: u64, mut callback: F) -> Result<()>
881 where
882 F: FnMut(PathBuf),
883 {
884 self.run_internal(|_, path| callback(path), Some(Duration::from_millis(ms))).await
885 }
886
887 fn should_watch<F>(&self, state: &WatcherState<F>, relative_path: &Path, is_dir: bool) -> bool {
888 let segments = path_to_segments(relative_path);
889
890 if state.exclude_patterns.iter().any(|p| p.check(&segments, false)) {
891 return false;
892 }
893
894 state.include_patterns.iter().any(|p| p.check(&segments, is_dir))
895 }
896
897 fn check_event<F>(&self, state: &WatcherState<F>, rel_path: &Path, is_dir: bool) -> bool {
898 if if is_dir { !self.match_dirs } else { !self.match_files } {
899 return false;
900 }
901 self.should_watch(state, rel_path, false)
902 }
903
904 fn emit_event<F>(
905 &self,
906 state: &mut WatcherState<F>,
907 event: WatchEvent,
908 rel_path: &Path,
909 ) where
910 F: FnMut(WatchEvent, PathBuf),
911 {
912 let path = if self.return_absolute {
913 if rel_path.as_os_str().is_empty() {
914 state.root.clone()
915 } else {
916 state.root.join(rel_path)
917 }
918 } else {
919 rel_path.to_path_buf()
920 };
921 (state.callback)(event, path);
922 }
923
924 fn add_watch_recursive<F>(
925 &self,
926 state: &mut WatcherState<F>,
927 initial_path: PathBuf,
928 emit_initial: bool,
929 ) where
930 F: FnMut(WatchEvent, PathBuf),
931 {
932 if state.paths.contains(&initial_path) {
933 return;
934 }
935
936 let mut stack = vec![initial_path];
937 while let Some(rel_path) = stack.pop() {
938 if !self.should_watch(state, &rel_path, true) {
939 continue;
940 }
941
942 let full_path = if rel_path.as_os_str().is_empty() {
943 state.root.clone()
944 } else {
945 state.root.join(&rel_path)
946 };
947
948 if !full_path.is_dir() {
949 continue;
950 }
951
952 let wd = match state.inotify.add_watch(&full_path, INOTIFY_MASK) {
953 Ok(wd) => wd,
954 Err(e) => {
955 eprintln!("Failed to add watch for {:?}: {}", full_path, e);
956 continue;
957 }
958 };
959
960 state.paths.insert(rel_path.clone());
961 state.watches.insert(wd, rel_path.clone());
962
963 if self.debug_watches_enabled {
964 (state.callback)(WatchEvent::DebugWatch, rel_path.clone());
965 }
966
967 if let Ok(entries) = std::fs::read_dir(&full_path) {
968 for entry in entries.flatten() {
969 if let Ok(ft) = entry.file_type() {
970 let child_rel_path = rel_path.join(entry.file_name());
971 let is_dir = ft.is_dir();
972
973 if emit_initial && self.check_event(state, &child_rel_path, is_dir) {
974 self.emit_event(state, WatchEvent::Initial, &child_rel_path);
975 }
976
977 if is_dir && !state.paths.contains(&child_rel_path) {
978 stack.push(child_rel_path);
979 }
980 }
981 }
982 }
983 }
984 }
985
986 async fn run_internal<F>(self, callback: F, debounce: Option<Duration>) -> Result<()>
987 where
988 F: FnMut(WatchEvent, PathBuf),
989 {
990 let includes = if self.includes.is_empty() {
992 vec!["**".to_string()]
993 } else {
994 self.includes.clone()
995 };
996
997 if includes.is_empty() {
999 loop {
1000 tokio::time::sleep(Duration::from_secs(3600)).await;
1001 }
1002 }
1003
1004 let root = if self.base_dir.is_absolute() {
1005 self.base_dir.clone()
1006 } else {
1007 std::env::current_dir()
1008 .unwrap_or_else(|_| PathBuf::from("/"))
1009 .join(&self.base_dir)
1010 };
1011
1012 let mut state = WatcherState {
1013 root,
1014 inotify: Inotify::new()?,
1015 watches: HashMap::new(),
1016 paths: HashSet::new(),
1017 include_patterns: includes.iter().map(|p| Pattern::parse(p)).collect(),
1018 exclude_patterns: self.excludes.iter().map(|p| Pattern::parse(p)).collect(),
1019 callback,
1020 };
1021
1022 let emit_initial = self.watch_initial && debounce.is_none();
1024 self.add_watch_recursive(&mut state, PathBuf::new(), emit_initial);
1025
1026 let mut debounce_deadline: Option<tokio::time::Instant> = None;
1028 let mut debounce_first_path: Option<PathBuf> = None;
1029
1030 let mut buffer = [0u8; 8192];
1032 loop {
1033 let read_future = state.inotify.read_events(&mut buffer);
1035
1036 let read_result = if let Some(deadline) = debounce_deadline {
1037 let now = tokio::time::Instant::now();
1038 if deadline <= now {
1039 debounce_deadline = None;
1041 (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
1042 continue;
1043 }
1044 match tokio::time::timeout(deadline - now, read_future).await {
1046 Ok(result) => Some(result),
1047 Err(_) => {
1048 debounce_deadline = None;
1050 (state.callback)(WatchEvent::Update, debounce_first_path.take().unwrap_or_default());
1051 continue;
1052 }
1053 }
1054 } else {
1055 Some(read_future.await)
1056 };
1057
1058 let Some(result) = read_result else { continue };
1059
1060 match result {
1061 Ok(len) => {
1062 let events = parse_inotify_events(&buffer, len);
1063 let mut first_matching_path: Option<PathBuf> = None;
1064
1065 for (wd, mask, name_str) in events {
1066 if (mask & libc::IN_IGNORED) != 0 {
1067 if let Some(path) = state.watches.remove(&wd) {
1068 state.paths.remove(&path);
1069 }
1070 continue;
1071 }
1072
1073 let rel_path = if let Some(dir_path) = state.watches.get(&wd) {
1074 dir_path.join(&name_str)
1075 } else {
1076 println!("Warning: received event for unknown watch descriptor {}", wd);
1077 continue;
1078 };
1079
1080 let is_dir = mask & libc::IN_ISDIR != 0;
1081 let is_create = (mask & libc::IN_CREATE) != 0
1082 || (mask & libc::IN_MOVED_TO) != 0;
1083 let is_delete = (mask & libc::IN_DELETE) != 0
1084 || (mask & libc::IN_MOVED_FROM) != 0;
1085 let is_update = (mask & libc::IN_MODIFY) != 0
1086 || (mask & libc::IN_CLOSE_WRITE) != 0;
1087
1088 if is_dir && is_create {
1089 self.add_watch_recursive(&mut state, rel_path.clone(), false);
1091 }
1092
1093 let event_type = if is_create && self.watch_create {
1094 WatchEvent::Create
1095 } else if is_delete && self.watch_delete {
1096 WatchEvent::Delete
1097 } else if is_update && self.watch_update {
1098 WatchEvent::Update
1099 } else {
1100 continue
1101 };
1102
1103 if !self.check_event(&state, &rel_path, is_dir) {
1104 continue;
1105 }
1106
1107 if first_matching_path.is_none() {
1108 first_matching_path = Some(rel_path.clone());
1109 }
1110
1111 if debounce.is_none() {
1113 self.emit_event(&mut state, event_type, &rel_path);
1114 }
1115 }
1116
1117 if let Some(d) = debounce {
1119 if let Some(path) = first_matching_path {
1120 if debounce_first_path.is_none() {
1121 debounce_first_path = Some(path);
1122 }
1123 debounce_deadline = Some(tokio::time::Instant::now() + d);
1124 }
1125 }
1126 }
1127 Err(e) => {
1128 eprintln!("Error reading inotify events: {}", e);
1129 tokio::time::sleep(Duration::from_millis(100)).await;
1130 }
1131 }
1132 }
1133 }
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139 use std::collections::HashSet;
1140 use std::sync::{Arc, Mutex};
1141 use tokio::task::JoinHandle;
1142
1143 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1144 enum EventType {
1145 Create,
1146 Delete,
1147 Update,
1148 Initial,
1149 DebugWatch,
1150 }
1151
1152 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1153 struct Event {
1154 path: PathBuf,
1155 event_type: EventType,
1156 }
1157
1158 type EventTracker = Arc<Mutex<Vec<Event>>>;
1159
1160 struct TestInstance {
1161 test_dir: PathBuf,
1162 tracker: EventTracker,
1163 watcher_handle: Option<JoinHandle<()>>,
1164 }
1165
1166 impl TestInstance {
1167 async fn new<F>(test_name: &str, configure: F) -> Self
1168 where
1169 F: FnOnce(Watcher) -> Watcher + Send + 'static,
1170 {
1171 let test_dir = std::env::current_dir()
1172 .unwrap()
1173 .join(format!(".file-watcher-test-{}", test_name));
1174
1175 if test_dir.exists() {
1176 std::fs::remove_dir_all(&test_dir).unwrap();
1177 }
1178 std::fs::create_dir(&test_dir).unwrap();
1179
1180 let tracker = Arc::new(Mutex::new(Vec::new()));
1181
1182 let tracker_clone = tracker.clone();
1183 let test_dir_clone = test_dir.clone();
1184
1185 let watcher_handle = tokio::spawn(async move {
1186 let watcher = Watcher::new()
1187 .set_base_dir(&test_dir_clone)
1188 .debug_watches(true);
1189
1190 let watcher = configure(watcher);
1191
1192 let _ = watcher
1193 .run(move |event_type, path| {
1194 tracker_clone.lock().unwrap().push(Event {
1195 path: path.clone(),
1196 event_type: match event_type {
1197 WatchEvent::Create => EventType::Create,
1198 WatchEvent::Delete => EventType::Delete,
1199 WatchEvent::Update => EventType::Update,
1200 WatchEvent::Initial => EventType::Initial,
1201 WatchEvent::DebugWatch => EventType::DebugWatch,
1202 },
1203 });
1204 })
1205 .await;
1206 });
1207
1208 tokio::time::sleep(Duration::from_millis(100)).await;
1209
1210 let instance = Self {
1211 test_dir,
1212 tracker,
1213 watcher_handle: Some(watcher_handle),
1214 };
1215
1216 instance.assert_events(&[], &[], &[], &[""]).await;
1217
1218 instance
1219 }
1220
1221 fn create_dir(&self, path: &str) {
1222 std::fs::create_dir_all(self.test_dir.join(path)).unwrap();
1223 }
1224
1225 fn write_file(&self, path: &str, content: &str) {
1226 let full_path = self.test_dir.join(path);
1227 if let Some(parent) = full_path.parent() {
1228 std::fs::create_dir_all(parent).unwrap();
1229 }
1230 std::fs::write(full_path, content).unwrap();
1231 }
1232
1233 fn remove_file(&self, path: &str) {
1234 std::fs::remove_file(self.test_dir.join(path)).unwrap();
1235 }
1236
1237 fn rename(&self, from: &str, to: &str) {
1238 std::fs::rename(self.test_dir.join(from), self.test_dir.join(to)).unwrap();
1239 }
1240
1241 async fn assert_events(
1242 &self,
1243 creates: &[&str],
1244 deletes: &[&str],
1245 updates: &[&str],
1246 watches: &[&str],
1247 ) {
1248 tokio::time::sleep(Duration::from_millis(200)).await;
1249
1250 let events = self.tracker.lock().unwrap().clone();
1251 let mut expected = HashSet::new();
1252
1253 for create in creates {
1254 expected.insert(Event {
1255 path: PathBuf::from(create),
1256 event_type: EventType::Create,
1257 });
1258 }
1259
1260 for delete in deletes {
1261 expected.insert(Event {
1262 path: PathBuf::from(delete),
1263 event_type: EventType::Delete,
1264 });
1265 }
1266
1267 for update in updates {
1268 expected.insert(Event {
1269 path: PathBuf::from(update),
1270 event_type: EventType::Update,
1271 });
1272 }
1273
1274 for watch in watches {
1275 expected.insert(Event {
1276 path: PathBuf::from(watch),
1277 event_type: EventType::DebugWatch,
1278 });
1279 }
1280
1281 let actual: HashSet<Event> = events.iter().cloned().collect();
1282
1283 for event in &actual {
1284 if !expected.contains(event) {
1285 panic!("Unexpected event: {:?}", event);
1286 }
1287 }
1288
1289 for event in &expected {
1290 if !actual.contains(event) {
1291 panic!(
1292 "Missing expected event: {:?}\nActual events: {:?}",
1293 event, actual
1294 );
1295 }
1296 }
1297
1298 self.tracker.lock().unwrap().clear();
1299 }
1300
1301 async fn assert_no_events(&self) {
1302 tokio::time::sleep(Duration::from_millis(500)).await;
1303 let events = self.tracker.lock().unwrap();
1304 assert_eq!(
1305 events.len(),
1306 0,
1307 "Expected no events, but got: {:?}",
1308 events
1309 );
1310 }
1311 }
1312
1313 impl Drop for TestInstance {
1314 fn drop(&mut self) {
1315 if let Some(handle) = self.watcher_handle.take() {
1316 handle.abort();
1317 }
1318 if self.test_dir.exists() {
1319 let _ = std::fs::remove_dir_all(&self.test_dir);
1320 }
1321 }
1322 }
1323
1324 #[tokio::test]
1325 async fn test_file_create_update_delete() {
1326 let test = TestInstance::new("create_update_delete", |b| b.add_include("**/*")).await;
1327
1328 test.write_file("test.txt", "");
1329 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1330 .await;
1331
1332 test.write_file("test.txt", "hello");
1333 test.assert_events(&[], &[], &["test.txt"], &[]).await;
1334
1335 test.remove_file("test.txt");
1336 test.assert_events(&[], &["test.txt"], &[], &[]).await;
1337 }
1338
1339 #[tokio::test]
1340 async fn test_directory_operations() {
1341 let test = TestInstance::new("directory_operations", |b| b.add_include("**/*")).await;
1342
1343 test.create_dir("subdir");
1344 test.assert_events(&["subdir"], &[], &[], &["subdir"]).await;
1345
1346 test.write_file("subdir/file.txt", "");
1347 test.assert_events(&["subdir/file.txt"], &[], &["subdir/file.txt"], &[])
1348 .await;
1349 }
1350
1351 #[tokio::test]
1352 async fn test_move_operations() {
1353 let test = TestInstance::new("move_operations", |b| b.add_include("**/*")).await;
1354
1355 test.write_file("old.txt", "content");
1356 test.assert_events(&["old.txt"], &[], &["old.txt"], &[])
1357 .await;
1358
1359 test.rename("old.txt", "new.txt");
1360 test.assert_events(&["new.txt"], &["old.txt"], &[], &[])
1361 .await;
1362 }
1363
1364 #[tokio::test]
1365 async fn test_event_filtering() {
1366 let test = TestInstance::new("event_filtering", |b| {
1367 b.add_include("**/*")
1368 .watch_create(true)
1369 .watch_delete(false)
1370 .watch_update(false)
1371 })
1372 .await;
1373
1374 test.write_file("test.txt", "");
1375 test.assert_events(&["test.txt"], &[], &[], &[]).await;
1376
1377 test.write_file("test.txt", "hello");
1378 test.assert_no_events().await;
1379
1380 test.remove_file("test.txt");
1381 test.assert_no_events().await;
1382 }
1383
1384 #[tokio::test]
1385 async fn test_pattern_matching() {
1386 let test = TestInstance::new("pattern_matching", |b| b.add_include("**/*.txt")).await;
1387
1388 test.write_file("test.txt", "");
1389 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1390 .await;
1391
1392 test.write_file("test.rs", "");
1393 test.assert_no_events().await;
1394 }
1395
1396 #[tokio::test]
1397 async fn test_matching_stops_at_depth() {
1398 let test = TestInstance::new("matching_stops_at_depth", |b| b.add_include("*/xyz/*.*")).await;
1399
1400 test.write_file("test.txt", "");
1401 test.assert_no_events().await;
1402
1403 test.create_dir("abc/xyz");
1404 test.assert_events(&[], &[], &[], &["abc", "abc/xyz"]).await;
1405
1406 test.create_dir("abc/hjk/a.b");
1407 test.assert_no_events().await;
1408
1409 test.create_dir("abc/xyz/a.b");
1410 test.assert_events(&["abc/xyz/a.b"], &[], &[], &[]).await; test.create_dir("abc/xyz/a.b/x.y");
1413 test.assert_events(&[], &[], &[], &[]).await;
1414 }
1415
1416 #[tokio::test]
1417 async fn test_exclude_prevents_watching() {
1418 let test = TestInstance::new("exclude_prevents_watch", |b| {
1419 b.add_include("**/*").add_exclude("node_modules/**")
1420 })
1421 .await;
1422
1423 test.create_dir("node_modules");
1424 tokio::time::sleep(Duration::from_millis(200)).await;
1425
1426 test.write_file("node_modules/package.json", "");
1427 test.assert_no_events().await;
1428
1429 test.write_file("test.txt", "");
1430 test.assert_events(&["test.txt"], &[], &["test.txt"], &[])
1431 .await;
1432 }
1433
1434 #[tokio::test]
1435 async fn test_pattern_file() {
1436 let test_dir = std::env::current_dir()
1438 .unwrap()
1439 .join(".file-watcher-test-pattern_file");
1440
1441 if test_dir.exists() {
1442 std::fs::remove_dir_all(&test_dir).unwrap();
1443 }
1444 std::fs::create_dir(&test_dir).unwrap();
1445
1446 std::fs::write(
1448 test_dir.join(".watchignore"),
1449 "# Comment line\nignored/**\n",
1450 )
1451 .unwrap();
1452
1453 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1455 let tracker_clone = tracker.clone();
1456 let test_dir_clone = test_dir.clone();
1457
1458 let watcher_handle = tokio::spawn(async move {
1459 let _ = Watcher::new()
1460 .set_base_dir(&test_dir_clone)
1461 .debug_watches(true)
1462 .add_include("**/*")
1463 .add_ignore_file(".watchignore")
1464 .run(move |event_type, path| {
1465 tracker_clone.lock().unwrap().push(Event {
1466 path: path.clone(),
1467 event_type: match event_type {
1468 WatchEvent::Create => EventType::Create,
1469 WatchEvent::Delete => EventType::Delete,
1470 WatchEvent::Update => EventType::Update,
1471 WatchEvent::Initial => EventType::Initial,
1472 WatchEvent::DebugWatch => EventType::DebugWatch,
1473 },
1474 });
1475 })
1476 .await;
1477 });
1478
1479 tokio::time::sleep(Duration::from_millis(100)).await;
1480 tracker.lock().unwrap().clear(); std::fs::create_dir(test_dir.join("ignored")).unwrap();
1484 tokio::time::sleep(Duration::from_millis(200)).await;
1485
1486 std::fs::write(test_dir.join("ignored/test.txt"), "").unwrap();
1488 tokio::time::sleep(Duration::from_millis(200)).await;
1489
1490 {
1492 let events = tracker.lock().unwrap();
1493 let has_ignored_events = events.iter().any(|e| {
1494 e.path.to_string_lossy().contains("ignored")
1495 && e.event_type != EventType::DebugWatch
1496 });
1497 assert!(
1498 !has_ignored_events,
1499 "Expected no events for ignored files, but got: {:?}",
1500 events
1501 );
1502 }
1503 tracker.lock().unwrap().clear();
1504
1505 std::fs::write(test_dir.join("normal.txt"), "").unwrap();
1507 tokio::time::sleep(Duration::from_millis(200)).await;
1508
1509 {
1510 let events = tracker.lock().unwrap();
1511 let has_normal = events
1512 .iter()
1513 .any(|e| e.path == Path::new("normal.txt"));
1514 assert!(has_normal, "Expected event for normal.txt, got: {:?}", events);
1515 }
1516
1517 watcher_handle.abort();
1519 let _ = std::fs::remove_dir_all(&test_dir);
1520 }
1521
1522 #[tokio::test]
1523 async fn test_watch_initial() {
1524 let test_dir = std::env::current_dir()
1526 .unwrap()
1527 .join(".file-watcher-test-watch_initial");
1528
1529 if test_dir.exists() {
1530 std::fs::remove_dir_all(&test_dir).unwrap();
1531 }
1532 std::fs::create_dir(&test_dir).unwrap();
1533
1534 std::fs::write(test_dir.join("existing1.txt"), "content1").unwrap();
1536 std::fs::write(test_dir.join("existing2.txt"), "content2").unwrap();
1537 std::fs::create_dir(test_dir.join("subdir")).unwrap();
1538 std::fs::write(test_dir.join("subdir/nested.txt"), "nested").unwrap();
1539 std::fs::write(test_dir.join("ignored.rs"), "should be ignored").unwrap();
1540
1541 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1542 let tracker_clone = tracker.clone();
1543 let test_dir_clone = test_dir.clone();
1544
1545 let watcher_handle = tokio::spawn(async move {
1546 let _ = Watcher::new()
1547 .set_base_dir(&test_dir_clone)
1548 .add_include("**/*.txt")
1549 .watch_initial(true)
1550 .run(move |event_type, path| {
1551 tracker_clone.lock().unwrap().push(Event {
1552 path: path.clone(),
1553 event_type: match event_type {
1554 WatchEvent::Create => EventType::Create,
1555 WatchEvent::Delete => EventType::Delete,
1556 WatchEvent::Update => EventType::Update,
1557 WatchEvent::Initial => EventType::Initial,
1558 WatchEvent::DebugWatch => EventType::DebugWatch,
1559 },
1560 });
1561 })
1562 .await;
1563 });
1564
1565 tokio::time::sleep(Duration::from_millis(200)).await;
1566
1567 {
1569 let events = tracker.lock().unwrap();
1570 let initial_events: Vec<_> = events
1571 .iter()
1572 .filter(|e| e.event_type == EventType::Initial)
1573 .collect();
1574
1575 assert_eq!(
1576 initial_events.len(),
1577 3,
1578 "Expected 3 Initial events, got: {:?}",
1579 initial_events
1580 );
1581
1582 let paths: HashSet<_> = initial_events.iter().map(|e| e.path.clone()).collect();
1583 assert!(paths.contains(&PathBuf::from("existing1.txt")));
1584 assert!(paths.contains(&PathBuf::from("existing2.txt")));
1585 assert!(paths.contains(&PathBuf::from("subdir/nested.txt")));
1586
1587 assert!(!events.iter().any(|e| e.path.to_string_lossy().contains("ignored.rs")));
1589 }
1590
1591 tracker.lock().unwrap().clear();
1592
1593 std::fs::write(test_dir.join("new.txt"), "new content").unwrap();
1595 tokio::time::sleep(Duration::from_millis(200)).await;
1596
1597 {
1598 let events = tracker.lock().unwrap();
1599 let has_create = events
1600 .iter()
1601 .any(|e| e.path == Path::new("new.txt") && e.event_type == EventType::Create);
1602 assert!(has_create, "Expected Create event for new.txt, got: {:?}", events);
1603 }
1604
1605 watcher_handle.abort();
1607 let _ = std::fs::remove_dir_all(&test_dir);
1608 }
1609
1610 #[tokio::test]
1611 async fn test_watch_initial_with_dirs() {
1612 let test_dir = std::env::current_dir()
1614 .unwrap()
1615 .join(".file-watcher-test-watch_initial_dirs");
1616
1617 if test_dir.exists() {
1618 std::fs::remove_dir_all(&test_dir).unwrap();
1619 }
1620 std::fs::create_dir(&test_dir).unwrap();
1621
1622 std::fs::write(test_dir.join("file.txt"), "content").unwrap();
1624 std::fs::create_dir(test_dir.join("mydir")).unwrap();
1625
1626 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1627 let tracker_clone = tracker.clone();
1628 let test_dir_clone = test_dir.clone();
1629
1630 let watcher_handle = tokio::spawn(async move {
1631 let _ = Watcher::new()
1632 .set_base_dir(&test_dir_clone)
1633 .add_include("**/*")
1634 .watch_initial(true)
1635 .match_files(true)
1636 .match_dirs(false) .run(move |event_type, path| {
1638 tracker_clone.lock().unwrap().push(Event {
1639 path: path.clone(),
1640 event_type: match event_type {
1641 WatchEvent::Create => EventType::Create,
1642 WatchEvent::Delete => EventType::Delete,
1643 WatchEvent::Update => EventType::Update,
1644 WatchEvent::Initial => EventType::Initial,
1645 WatchEvent::DebugWatch => EventType::DebugWatch,
1646 },
1647 });
1648 })
1649 .await;
1650 });
1651
1652 tokio::time::sleep(Duration::from_millis(200)).await;
1653
1654 {
1655 let events = tracker.lock().unwrap();
1656 let initial_events: Vec<_> = events
1657 .iter()
1658 .filter(|e| e.event_type == EventType::Initial)
1659 .collect();
1660
1661 assert_eq!(
1663 initial_events.len(),
1664 1,
1665 "Expected 1 Initial event (file only), got: {:?}",
1666 initial_events
1667 );
1668 assert_eq!(initial_events[0].path, PathBuf::from("file.txt"));
1669 }
1670
1671 watcher_handle.abort();
1673 let _ = std::fs::remove_dir_all(&test_dir);
1674 }
1675
1676 #[tokio::test]
1677 async fn test_watch_initial_disabled_by_default() {
1678 let test_dir = std::env::current_dir()
1680 .unwrap()
1681 .join(".file-watcher-test-watch_initial_disabled");
1682
1683 if test_dir.exists() {
1684 std::fs::remove_dir_all(&test_dir).unwrap();
1685 }
1686 std::fs::create_dir(&test_dir).unwrap();
1687
1688 std::fs::write(test_dir.join("existing.txt"), "content").unwrap();
1690
1691 let tracker = Arc::new(Mutex::new(Vec::<Event>::new()));
1692 let tracker_clone = tracker.clone();
1693 let test_dir_clone = test_dir.clone();
1694
1695 let watcher_handle = tokio::spawn(async move {
1696 let _ = Watcher::new()
1697 .set_base_dir(&test_dir_clone)
1698 .add_include("**/*.txt")
1699 .run(move |event_type, path| {
1701 tracker_clone.lock().unwrap().push(Event {
1702 path: path.clone(),
1703 event_type: match event_type {
1704 WatchEvent::Create => EventType::Create,
1705 WatchEvent::Delete => EventType::Delete,
1706 WatchEvent::Update => EventType::Update,
1707 WatchEvent::Initial => EventType::Initial,
1708 WatchEvent::DebugWatch => EventType::DebugWatch,
1709 },
1710 });
1711 })
1712 .await;
1713 });
1714
1715 tokio::time::sleep(Duration::from_millis(200)).await;
1716
1717 {
1718 let events = tracker.lock().unwrap();
1719 let initial_events: Vec<_> = events
1720 .iter()
1721 .filter(|e| e.event_type == EventType::Initial)
1722 .collect();
1723
1724 assert_eq!(
1726 initial_events.len(),
1727 0,
1728 "Expected no Initial events when watch_initial is disabled, got: {:?}",
1729 initial_events
1730 );
1731 }
1732
1733 watcher_handle.abort();
1735 let _ = std::fs::remove_dir_all(&test_dir);
1736 }
1737}
1738
1739#[cfg(test)]
1740mod matcher_tests {
1741 use super::Matcher;
1742
1743 fn matcher(includes: &[&str], excludes: &[&str]) -> Matcher {
1744 Matcher::new(includes, excludes)
1745 }
1746
1747 #[test]
1748 fn matches_everything_by_default() {
1749 let m = matcher(&["**/*"], &[]);
1750 assert!(m.matches("index.html"));
1751 assert!(m.matches("src/deep/file.rs"));
1752 }
1753
1754 #[test]
1755 fn patterns_without_a_slash_match_at_any_depth() {
1756 let m = matcher(&["**/*"], &["node_modules", "*.log"]);
1757 assert!(!m.matches("node_modules/x/y.js"));
1758 assert!(!m.matches("deep/node_modules/x.js"));
1759 assert!(!m.matches("app.log"));
1760 assert!(!m.matches("var/logs/app.log"));
1761 assert!(m.matches("app.rs"));
1762 }
1763
1764 #[test]
1765 fn a_leading_slash_anchors_to_the_root() {
1766 let m = matcher(&["**/*"], &["/webcentral.conf", "/_data"]);
1767 assert!(!m.matches("webcentral.conf"));
1768 assert!(!m.matches("_data/log/x.log"));
1769 assert!(m.matches("sub/webcentral.conf"));
1770 }
1771
1772 #[test]
1773 fn naming_a_directory_covers_its_contents() {
1774 let m = matcher(&["src", "config.yaml"], &[]);
1775 assert!(m.matches("src"));
1776 assert!(m.matches("src/main.rs"));
1777 assert!(m.matches("src/deep/main.rs"));
1778 assert!(m.matches("config.yaml"));
1779 assert!(!m.matches("other.txt"));
1780 }
1781
1782 #[test]
1783 fn double_wildcards_span_directories() {
1784 let m = matcher(&["src/**"], &[]);
1785 assert!(m.matches("src/main.rs"));
1786 assert!(m.matches("src/deep/main.rs"));
1787 assert!(!m.matches("other/main.rs"));
1788 }
1789
1790 #[test]
1791 fn wildcards_within_a_segment() {
1792 let m = matcher(&["*.py", "test-?.txt", "[abc]*.md"], &[]);
1793 assert!(m.matches("app.py"));
1794 assert!(m.matches("deep/app.py"));
1795 assert!(!m.matches("app.pyc"));
1796 assert!(m.matches("test-1.txt"));
1797 assert!(!m.matches("test-12.txt"));
1798 assert!(m.matches("a-file.md"));
1799 assert!(!m.matches("z-file.md"));
1800 }
1801
1802 #[test]
1803 fn negated_and_ranged_classes() {
1804 let m = matcher(&["[!abc]*.md", "x[0-9].txt"], &[]);
1805 assert!(m.matches("z.md"));
1806 assert!(!m.matches("a.md"));
1807 assert!(m.matches("x5.txt"));
1808 assert!(!m.matches("xa.txt"));
1809 }
1810
1811 #[test]
1812 fn excludes_win_over_includes() {
1813 let m = matcher(&["src/**"], &["src/build"]);
1814 assert!(m.matches("src/main.rs"));
1815 assert!(!m.matches("src/build"));
1816 assert!(!m.matches("src/build/out.js"));
1817 }
1818
1819 #[test]
1820 fn hidden_files_and_swap_files() {
1821 let m = matcher(&["**/*"], &[".*", "*.sw?"]);
1822 assert!(!m.matches(".env"));
1823 assert!(!m.matches(".git/config"));
1824 assert!(!m.matches("src/main.swp"));
1825 assert!(m.matches("visible.txt"));
1826 assert!(m.matches("main.sw"));
1827 }
1828}