1use crate::error::Error;
2use crate::index::constraints::{GlobPattern, compile_one, glob_matches_into};
3use parking_lot::Mutex;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::sync::mpsc;
8use tracing::{debug, error};
9
10#[repr(transparent)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct WatchId(pub u64);
14
15pub(crate) type WatchCallback = Box<dyn Fn(WatchId, &[WatchEvent]) + Send + Sync>;
16
17#[repr(u8)]
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum WatchEventKind {
24 Created = 0,
25 Modified = 1,
26 Removed = 2,
27 Rescan = 3,
29 Renamed = 4,
31}
32
33impl WatchEventKind {
34 pub fn as_str(&self) -> &'static str {
35 match self {
36 WatchEventKind::Created => "created",
37 WatchEventKind::Modified => "modified",
38 WatchEventKind::Removed => "removed",
39 WatchEventKind::Rescan => "rescan",
40 WatchEventKind::Renamed => "renamed",
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct WatchEvent {
48 pub path: PathBuf,
50 pub kind: WatchEventKind,
51 pub from: Option<PathBuf>,
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct WatchOptions {
58 pub ignore: Vec<String>,
60}
61
62type WatchMask = u128;
63const MAX_BATCH_EVENTS: usize = WatchMask::BITS as usize;
64
65pub(crate) struct RawWatchEvent {
66 pub(crate) path: PathBuf,
67 pub(crate) kind: WatchEventKind,
68 pub(crate) is_ignored: bool,
69 pub(crate) from: Option<PathBuf>,
70}
71
72enum WatchMatcher {
73 Glob(GlobPattern),
74 Exact(PathBuf),
75 Dir(PathBuf),
76}
77
78impl WatchMatcher {
79 fn new(pattern: &str, base: &Path) -> Result<Self, Error> {
80 let pattern = pattern.trim();
81 if pattern.is_empty() {
82 return Ok(WatchMatcher::Dir(PathBuf::new()));
83 }
84
85 let Some(relative) = relative_pattern(pattern, base) else {
86 return Err(Error::InvalidGlobPattern {
87 pattern: pattern.to_string(),
88 reason: "watch patterns must be inside the indexed base path".into(),
89 });
90 };
91
92 if fff_query_parser::glob_detect::has_wildcards(pattern) {
93 let glob = relative.to_string_lossy().replace('\\', "/");
94 return compile_one(&glob).map(WatchMatcher::Glob).ok_or_else(|| {
95 Error::InvalidGlobPattern {
96 pattern: pattern.to_string(),
97 reason: "failed to compile glob".into(),
98 }
99 });
100 }
101
102 if base.join(&relative).is_dir() {
103 return Ok(WatchMatcher::Dir(relative));
104 }
105
106 Ok(WatchMatcher::Exact(relative))
107 }
108}
109
110#[derive(Default)]
111struct SubIgnore {
112 globs: Vec<GlobPattern>,
113 prefixes: Vec<PathBuf>,
114}
115
116impl SubIgnore {
117 fn prefix_matches(&self, path: &Path) -> bool {
118 self.prefixes.iter().any(|prefix| path.starts_with(prefix))
119 }
120}
121
122fn relative_pattern(pattern: &str, base: &Path) -> Option<PathBuf> {
123 let expanded = crate::path_utils::expand_tilde(pattern);
124 let relative = if expanded.is_absolute() || expanded.has_root() {
125 match expanded.strip_prefix(base) {
126 Ok(rel) => rel,
127 Err(_) => {
130 let canonical = crate::path_utils::canonicalize(&expanded).ok()?;
131 return relative_from_canonical(&canonical, base);
132 }
133 }
134 } else {
135 &expanded
136 };
137
138 reject_parent_components(relative)
139}
140
141fn relative_from_canonical(canonical: &Path, base: &Path) -> Option<PathBuf> {
142 let relative = canonical.strip_prefix(base).ok()?;
143 reject_parent_components(relative)
144}
145
146fn reject_parent_components(path: &Path) -> Option<PathBuf> {
147 if path
148 .components()
149 .any(|component| component == std::path::Component::ParentDir)
150 {
151 return None;
152 }
153
154 Some(path.components().collect())
155}
156
157fn resolve_sub_ignore(patterns: &[String], base: &Path) -> Result<SubIgnore, Error> {
158 let mut ignore = SubIgnore::default();
159
160 for pattern in patterns {
161 let pattern = pattern.trim();
162 if pattern.is_empty() {
163 continue;
164 }
165 let Some(relative) = relative_pattern(pattern, base) else {
166 return Err(Error::InvalidGlobPattern {
167 pattern: pattern.to_string(),
168 reason: "ignore patterns must be inside the indexed base path".into(),
169 });
170 };
171
172 if fff_query_parser::glob_detect::has_wildcards(pattern) {
173 match compile_one(&relative.to_string_lossy().replace('\\', "/")) {
174 Some(compiled) => ignore.globs.push(compiled),
175 None => {
176 return Err(Error::InvalidGlobPattern {
177 pattern: pattern.to_string(),
178 reason: "failed to compile ignore glob".into(),
179 });
180 }
181 }
182 } else {
183 ignore.prefixes.push(relative);
184 }
185 }
186
187 Ok(ignore)
188}
189
190struct WatchSub {
191 id: WatchId,
192 matcher: WatchMatcher,
193 ignore: SubIgnore,
194 callback: WatchCallback,
195 active: AtomicBool,
196 epoch: AtomicU64,
197}
198
199impl WatchSub {
200 fn filter_mask(&self, paths: &[&str], scratch: &mut Vec<usize>) -> WatchMask {
201 let mut mask = 0;
202
203 match &self.matcher {
204 WatchMatcher::Glob(g) => {
205 scratch.clear();
206 glob_matches_into(g, paths, scratch);
207 for &index in scratch.iter() {
208 mask |= 1 << index;
209 }
210 }
211 WatchMatcher::Dir(d) => {
212 for (index, path) in paths.iter().enumerate() {
213 if Path::new(path).starts_with(d) {
214 mask |= 1 << index;
215 }
216 }
217 }
218 WatchMatcher::Exact(p) => {
219 for (index, path) in paths.iter().enumerate() {
220 if Path::new(path) == p {
221 mask |= 1 << index;
222 }
223 }
224 }
225 }
226
227 for g in &self.ignore.globs {
229 scratch.clear();
230 glob_matches_into(g, paths, scratch);
231 for &index in scratch.iter() {
232 mask &= !(1 << index);
233 }
234 }
235 if !self.ignore.prefixes.is_empty() {
236 for (index, path) in paths.iter().enumerate() {
237 if self.ignore.prefix_matches(Path::new(path)) {
238 mask &= !(1 << index);
239 }
240 }
241 }
242
243 mask
244 }
245}
246
247struct CallbackDelivery {
248 sub: Arc<WatchSub>,
249 events: Vec<WatchEvent>,
250 epoch: u64,
251}
252
253enum CallbackMessage {
254 Deliver(Vec<CallbackDelivery>),
255 Drain(mpsc::Sender<()>),
257 Stop,
258}
259
260#[derive(Default)]
261struct CallbackDispatcherState {
262 sender: Option<mpsc::Sender<CallbackMessage>>,
263 thread: Option<std::thread::JoinHandle<()>>,
264}
265
266#[derive(Default)]
267struct CallbackDispatcher {
268 state: Mutex<CallbackDispatcherState>,
269}
270
271impl CallbackDispatcher {
272 fn start(&self) -> Result<(), Error> {
275 let mut state = self.state.lock();
276 if state.sender.is_some() {
277 return Ok(());
278 }
279
280 let (sender, receiver) = mpsc::channel();
281 let thread = std::thread::Builder::new()
282 .name("fff-watch-callback".into())
283 .spawn(move || {
284 while let Ok(message) = receiver.recv() {
285 match message {
286 CallbackMessage::Deliver(deliveries) => {
287 for delivery in deliveries {
288 if !delivery.sub.active.load(Ordering::Acquire)
289 || delivery.sub.epoch.load(Ordering::Acquire) != delivery.epoch
290 {
291 continue;
292 }
293
294 let id = delivery.sub.id;
295 let result =
296 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
297 (delivery.sub.callback)(id, &delivery.events)
298 }));
299 if result.is_err() {
300 error!(sub = id.0, "watch callback panicked");
301 }
302 }
303 }
304 CallbackMessage::Drain(done) => {
305 let _ = done.send(());
306 }
307 CallbackMessage::Stop => break,
308 }
309 }
310 })
311 .map_err(Error::WatchDispatcherStart)?;
312
313 state.sender = Some(sender);
314 state.thread = Some(thread);
315 Ok(())
316 }
317
318 fn deliver(&self, deliveries: Vec<CallbackDelivery>) {
319 if deliveries.is_empty() {
320 return;
321 }
322 let Some(sender) = self.state.lock().sender.clone() else {
323 error!("watch callback dispatcher is not running");
324 return;
325 };
326 if sender.send(CallbackMessage::Deliver(deliveries)).is_err() {
327 error!("watch callback dispatcher stopped unexpectedly");
328 }
329 }
330
331 fn drain(&self) {
332 let (sender, is_dispatch_thread) = {
333 let state = self.state.lock();
334 let Some(sender) = state.sender.as_ref() else {
335 return;
336 };
337 let is_dispatch_thread = state
338 .thread
339 .as_ref()
340 .is_some_and(|thread| thread.thread().id() == std::thread::current().id());
341 (sender.clone(), is_dispatch_thread)
342 };
343
344 if is_dispatch_thread {
345 return;
346 }
347
348 let (done_tx, done_rx) = mpsc::channel();
349 if sender.send(CallbackMessage::Drain(done_tx)).is_ok() {
350 let _ = done_rx.recv();
351 }
352 }
353}
354
355impl Drop for CallbackDispatcher {
356 fn drop(&mut self) {
357 let state = self.state.get_mut();
358 if let Some(sender) = state.sender.take() {
359 let _ = sender.send(CallbackMessage::Stop);
360 }
361
362 if let Some(thread) = state.thread.take()
363 && thread.thread().id() != std::thread::current().id()
364 {
365 let _ = thread.join();
366 }
367 }
368}
369
370#[derive(Default)]
371struct WatchRegistryState {
372 subs: Vec<Arc<WatchSub>>,
373 base_path: Option<PathBuf>,
374 epoch: u64,
375}
376
377#[derive(Default)]
379pub(crate) struct WatchRegistry {
380 state: Mutex<WatchRegistryState>,
381 dispatcher: CallbackDispatcher,
382}
383
384static NEXT_WATCH_ID: AtomicU64 = AtomicU64::new(1);
386
387impl WatchRegistry {
388 #[inline]
389 pub(crate) fn is_active(&self) -> bool {
390 !self.state.lock().subs.is_empty()
391 }
392
393 pub(crate) fn subscribe(
394 &self,
395 base_path: &Path,
396 pattern: &str,
397 options: WatchOptions,
398 callback: WatchCallback,
399 ) -> Result<WatchId, Error> {
400 let matcher = WatchMatcher::new(pattern, base_path)?;
401 let ignore = resolve_sub_ignore(&options.ignore, base_path)?;
402
403 let mut state = self.state.lock();
404 if state.base_path.as_deref() != Some(base_path) {
405 return Err(Error::WatchBaseChanged);
406 }
407 self.dispatcher.start()?;
408
409 let id = WatchId(NEXT_WATCH_ID.fetch_add(1, Ordering::Relaxed));
410 let sub = Arc::new(WatchSub {
411 id,
412 matcher,
413 ignore,
414 callback,
415 active: AtomicBool::new(true),
416 epoch: AtomicU64::new(state.epoch),
417 });
418
419 state.subs.push(sub);
420 Ok(id)
421 }
422
423 pub(crate) fn unsubscribe(&self, id: WatchId) -> bool {
424 let mut state = self.state.lock();
425 let Some(idx) = state.subs.iter().position(|s| s.id == id) else {
426 return false;
427 };
428 let sub = state.subs.swap_remove(idx);
429 sub.active.store(false, Ordering::Release);
430 drop(state);
431 drop(sub);
432 true
433 }
434
435 pub(crate) fn contains(&self, id: WatchId) -> bool {
436 self.state.lock().subs.iter().any(|sub| sub.id == id)
437 }
438
439 pub(crate) fn shutdown(&self) {
440 let mut state = self.state.lock();
441 let subs = std::mem::take(&mut state.subs);
442 for sub in &subs {
443 sub.active.store(false, Ordering::Release);
444 }
445 drop(state);
446 }
447
448 pub(crate) fn shutdown_and_wait(&self) {
449 self.shutdown();
450 self.dispatcher.drain();
451 }
452
453 pub(crate) fn rebase(&self, base_path: &Path) {
454 let mut state = self.state.lock();
455 if state.base_path.as_deref() == Some(base_path) {
456 return;
457 }
458
459 state.base_path = Some(base_path.to_path_buf());
460 state.epoch = state.epoch.wrapping_add(1);
461 for sub in &state.subs {
462 sub.epoch.store(state.epoch, Ordering::Release);
463 }
464 drop(state);
465 self.dispatcher.drain();
466 }
467
468 pub(crate) fn dispatch(&self, base_path: &Path, events: Vec<RawWatchEvent>) {
469 if events.is_empty() {
470 return;
471 }
472
473 let state = self.state.lock();
474 if state.subs.is_empty() || state.base_path.as_deref() != Some(base_path) {
475 return;
476 }
477
478 for batch in events.chunks(MAX_BATCH_EVENTS) {
479 let mut paths = Vec::with_capacity(batch.len());
480 let mut visible_mask = 0;
481 let mut rescan_mask = 0;
482 let has_renames = batch.iter().any(|event| event.from.is_some());
483 let mut from_paths = Vec::with_capacity(if has_renames { batch.len() } else { 0 });
486
487 for (index, event) in batch.iter().enumerate() {
488 let relative = event
489 .path
490 .strip_prefix(base_path)
491 .expect("watch event path must be inside the indexed base path");
492 paths.push(relative.to_string_lossy().replace('\\', "/"));
493
494 if has_renames {
495 let source = event
496 .from
497 .as_deref()
498 .and_then(|from| from.strip_prefix(base_path).ok())
499 .map(|rel| rel.to_string_lossy().replace('\\', "/"));
500 from_paths.push(source.unwrap_or_else(|| paths[index].clone()));
501 }
502
503 let bit = 1 << index;
504 if event.kind == WatchEventKind::Rescan {
505 rescan_mask |= bit;
506 } else if !event.is_ignored {
507 visible_mask |= bit;
508 }
509 }
510
511 let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
512 let from_refs: Vec<&str> = from_paths.iter().map(String::as_str).collect();
513 let mut scratch = Vec::new();
514 let mut deliveries = Vec::with_capacity(state.subs.len());
515 for sub in &state.subs {
516 let mut matched = sub.filter_mask(&path_refs, &mut scratch);
517 if has_renames {
518 matched |= sub.filter_mask(&from_refs, &mut scratch);
519 }
520 let mut delivery_mask = (matched & visible_mask) | rescan_mask;
521 if delivery_mask == 0 {
522 continue;
523 }
524
525 let mut filtered = Vec::with_capacity(delivery_mask.count_ones() as usize);
526 while delivery_mask != 0 {
527 let index = delivery_mask.trailing_zeros() as usize;
528 let event = &batch[index];
529 filtered.push(WatchEvent {
530 path: event.path.clone(),
531 kind: event.kind,
532 from: event.from.clone(),
533 });
534 delivery_mask &= delivery_mask - 1;
535 }
536
537 debug!(
538 sub = sub.id.0,
539 count = filtered.len(),
540 "queueing watch events"
541 );
542 deliveries.push(CallbackDelivery {
543 sub: Arc::clone(sub),
544 events: filtered,
545 epoch: state.epoch,
546 });
547 }
548 self.dispatcher.deliver(deliveries);
549 }
550 }
551
552 pub(crate) fn dispatch_rescan(&self, base_path: &Path) {
554 self.dispatch(
555 base_path,
556 vec![RawWatchEvent {
557 path: base_path.to_path_buf(),
558 kind: WatchEventKind::Rescan,
559 is_ignored: false,
560 from: None,
561 }],
562 );
563 }
564}
565
566impl Drop for WatchRegistry {
567 fn drop(&mut self) {
568 self.shutdown();
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use parking_lot::{Condvar, Mutex};
576 use std::sync::atomic::{AtomicBool, AtomicUsize};
577 use std::time::Duration;
578
579 fn raw(path: &str, kind: WatchEventKind, is_ignored: bool) -> RawWatchEvent {
580 RawWatchEvent {
581 path: PathBuf::from(path),
582 kind,
583 is_ignored,
584 from: None,
585 }
586 }
587
588 fn registry(base: &Path) -> Arc<WatchRegistry> {
589 let registry = Arc::new(WatchRegistry::default());
590 registry.rebase(base);
591 registry
592 }
593
594 type Collected = Arc<Mutex<Vec<WatchEvent>>>;
595
596 fn collector() -> (WatchCallback, Collected) {
598 let collected: Collected = Arc::new(Mutex::new(Vec::new()));
599 let sink = Arc::clone(&collected);
600 let cb: WatchCallback = Box::new(move |_id, events| sink.lock().extend_from_slice(events));
601 (cb, collected)
602 }
603
604 fn wait_events(collected: &Collected, n: usize) -> Vec<WatchEvent> {
606 for _ in 0..1000 {
607 if collected.lock().len() >= n {
608 break;
609 }
610 std::thread::sleep(std::time::Duration::from_millis(5));
611 }
612 collected.lock().clone()
613 }
614
615 #[test]
616 fn dispatcher_starts_on_first_subscription() {
617 let base = Path::new("/repo");
618 let registry = WatchRegistry::default();
619 registry.rebase(base);
620
621 assert!(registry.dispatcher.state.lock().thread.is_none());
622 let (cb, _) = collector();
623 registry
624 .subscribe(base, "**", WatchOptions::default(), cb)
625 .unwrap();
626 assert!(registry.dispatcher.state.lock().thread.is_some());
627 }
628
629 #[test]
630 fn resolve_relative_glob() {
631 let base = Path::new("/repo");
632 assert!(matches!(
633 WatchMatcher::new("./**/*.rs", base).unwrap(),
634 WatchMatcher::Glob(_)
635 ));
636 assert!(matches!(
637 WatchMatcher::new("src/*.ts", base).unwrap(),
638 WatchMatcher::Glob(_)
639 ));
640 }
641
642 #[test]
643 fn reject_absolute_glob_outside_base() {
644 assert!(WatchMatcher::new("/other/**/*.rs", Path::new("/repo")).is_err());
645 }
646
647 #[test]
648 fn resolve_exact_paths() {
649 let base = std::env::temp_dir();
650 let inside = base.join("some_file.txt");
651 match WatchMatcher::new(inside.to_str().unwrap(), &base).unwrap() {
652 WatchMatcher::Exact(path) => assert_eq!(path, Path::new("some_file.txt")),
653 _ => panic!("expected exact"),
654 }
655 match WatchMatcher::new("relative_file.txt", &base).unwrap() {
656 WatchMatcher::Exact(path) => assert_eq!(path, Path::new("relative_file.txt")),
657 _ => panic!("expected exact"),
658 }
659 assert!(WatchMatcher::new("../outside", &base).is_err());
660 }
661
662 #[test]
663 fn resolve_existing_dir_as_subtree() {
664 let tmp = tempfile::TempDir::new().unwrap();
665 let base = tmp.path().to_path_buf();
666 std::fs::create_dir(base.join("src")).unwrap();
667
668 match WatchMatcher::new("src", &base).unwrap() {
669 WatchMatcher::Dir(path) => assert_eq!(path, Path::new("src")),
670 _ => panic!("expected dir"),
671 }
672 match WatchMatcher::new(base.to_str().unwrap(), &base).unwrap() {
673 WatchMatcher::Dir(path) => assert!(path.as_os_str().is_empty()),
674 _ => panic!("expected dir"),
675 }
676 }
677
678 #[test]
679 fn resolve_empty_pattern_as_whole_tree() {
680 let tmp = tempfile::TempDir::new().unwrap();
681 let base = tmp.path().to_path_buf();
682
683 for pattern in ["", " "] {
684 match WatchMatcher::new(pattern, &base).unwrap() {
685 WatchMatcher::Dir(path) => assert!(path.as_os_str().is_empty()),
686 _ => panic!("expected dir"),
687 }
688 }
689 }
690
691 #[test]
692 fn mixed_batch_dispatch_preserves_order_and_filters() {
693 let base = Path::new("/repo");
694 let registry = registry(base);
695
696 let (glob_cb, glob_events) = collector();
697 registry
698 .subscribe(
699 base,
700 "**/*.rs",
701 WatchOptions {
702 ignore: vec!["src/vendor".into(), "*.gen.rs".into()],
703 },
704 glob_cb,
705 )
706 .unwrap();
707 let (dir_cb, dir_events) = collector();
708 registry
709 .subscribe(base, "src/**", WatchOptions::default(), dir_cb)
710 .unwrap();
711 let (exact_cb, exact_events) = collector();
712 registry
713 .subscribe(base, "dist/out.js", WatchOptions::default(), exact_cb)
714 .unwrap();
715
716 registry.dispatch(
717 base,
718 vec![
719 raw("/repo/src/a.rs", WatchEventKind::Created, false),
720 raw("/repo/src/b.gen.rs", WatchEventKind::Modified, false),
721 raw("/repo/src/vendor/c.rs", WatchEventKind::Modified, false),
722 raw("/repo/lib/d.rs", WatchEventKind::Removed, false),
723 raw("/repo/dist/out.js", WatchEventKind::Modified, true), raw("/repo/src/e.txt", WatchEventKind::Created, true), ],
726 );
727
728 let glob = wait_events(&glob_events, 2);
729 let paths: Vec<_> = glob.iter().map(|e| e.path.clone()).collect();
730 assert_eq!(
731 paths,
732 vec![
733 PathBuf::from("/repo/src/a.rs"),
734 PathBuf::from("/repo/lib/d.rs"),
735 ]
736 );
737
738 let dir = wait_events(&dir_events, 3);
739 let paths: Vec<_> = dir.iter().map(|e| e.path.clone()).collect();
740 assert_eq!(
741 paths,
742 vec![
743 PathBuf::from("/repo/src/a.rs"),
744 PathBuf::from("/repo/src/b.gen.rs"),
745 PathBuf::from("/repo/src/vendor/c.rs"),
746 ]
747 );
748
749 assert!(exact_events.lock().is_empty());
750 }
751
752 #[test]
753 fn rescan_is_broadcast_to_every_subscription() {
754 let base = Path::new("/repo");
755 let registry = registry(base);
756 let (glob_cb, glob_events) = collector();
757 let (exact_cb, exact_events) = collector();
758 registry
759 .subscribe(base, "src/**", WatchOptions::default(), glob_cb)
760 .unwrap();
761 registry
762 .subscribe(base, "dist/out.js", WatchOptions::default(), exact_cb)
763 .unwrap();
764
765 registry.dispatch_rescan(base);
766
767 for events in [&glob_events, &exact_events] {
768 let events = wait_events(events, 1);
769 assert_eq!(events.len(), 1);
770 assert_eq!(events[0].path, base);
771 assert_eq!(events[0].kind, WatchEventKind::Rescan);
772 }
773 }
774
775 #[test]
776 fn registry_dispatch_matches_glob_and_batches() {
777 let base = Path::new("/repo");
778 let registry = registry(base);
779 let hits = Arc::new(Mutex::new(Vec::<WatchEvent>::new()));
780 let calls = Arc::new(AtomicUsize::new(0));
781
782 let hits_cb = Arc::clone(&hits);
783 let calls_cb = Arc::clone(&calls);
784 let id = registry
785 .subscribe(
786 base,
787 "**/*.rs",
788 WatchOptions::default(),
789 Box::new(move |_id, events| {
790 calls_cb.fetch_add(1, Ordering::SeqCst);
791 hits_cb.lock().extend_from_slice(events);
792 }),
793 )
794 .unwrap();
795
796 registry.dispatch(
797 base,
798 vec![
799 raw("/repo/src/a.rs", WatchEventKind::Modified, false),
800 raw("/repo/src/b.ts", WatchEventKind::Modified, false),
801 raw("/repo/target/c.rs", WatchEventKind::Created, true),
802 ],
803 );
804
805 for _ in 0..400 {
806 if calls.load(Ordering::SeqCst) == 1 {
807 break;
808 }
809 std::thread::sleep(Duration::from_millis(5));
810 }
811 let events = hits.lock();
812 assert_eq!(calls.load(Ordering::SeqCst), 1);
814 assert_eq!(events.len(), 1);
815 assert_eq!(events[0].path, PathBuf::from("/repo/src/a.rs"));
816
817 assert!(registry.unsubscribe(id));
818 assert!(!registry.is_active());
819 assert!(!registry.unsubscribe(id));
820 }
821
822 #[test]
823 fn large_batch_is_delivered_without_coalescing() {
824 let base = Path::new("/repo");
825 let registry = registry(base);
826
827 let collected: Collected = Arc::new(Mutex::new(Vec::new()));
828 let batch_sizes = Arc::new(Mutex::new(Vec::new()));
829 let collected_cb = Arc::clone(&collected);
830 let batch_sizes_cb = Arc::clone(&batch_sizes);
831 registry
832 .subscribe(
833 base,
834 "**/*.rs",
835 WatchOptions::default(),
836 Box::new(move |_, events| {
837 batch_sizes_cb.lock().push(events.len());
838 collected_cb.lock().extend_from_slice(events);
839 }),
840 )
841 .unwrap();
842
843 let events: Vec<RawWatchEvent> = (0..257)
844 .map(|i| {
845 raw(
846 &format!("/repo/src/f{i}.rs"),
847 WatchEventKind::Modified,
848 false,
849 )
850 })
851 .collect();
852 registry.dispatch(base, events);
853
854 let delivered = wait_events(&collected, 257);
855 assert_eq!(delivered.len(), 257);
856 assert!(
857 delivered
858 .iter()
859 .all(|event| event.kind == WatchEventKind::Modified)
860 );
861 assert_eq!(*batch_sizes.lock(), vec![128, 128, 1]);
862 }
863
864 #[test]
865 fn duplicate_paths_are_delivered_in_order() {
866 let base = Path::new("/repo");
867 let registry = registry(base);
868 let (cb, collected) = collector();
869 registry
870 .subscribe(base, "**", WatchOptions::default(), cb)
871 .unwrap();
872
873 registry.dispatch(
874 base,
875 vec![
876 raw("/repo/a.rs", WatchEventKind::Created, false),
877 raw("/repo/a.rs", WatchEventKind::Modified, false),
878 raw("/repo/a.rs", WatchEventKind::Removed, false),
879 ],
880 );
881
882 let delivered = wait_events(&collected, 3);
883 let kinds: Vec<_> = delivered.iter().map(|event| event.kind).collect();
884 assert_eq!(
885 kinds,
886 vec![
887 WatchEventKind::Created,
888 WatchEventKind::Modified,
889 WatchEventKind::Removed,
890 ]
891 );
892 }
893
894 #[test]
895 fn rebase_keeps_relative_subscriptions() {
896 let old_base = Path::new("/old");
897 let new_base = Path::new("/new");
898 let registry = registry(old_base);
899 let (cb, collected) = collector();
900 let id = registry
901 .subscribe(old_base, "src/**", WatchOptions::default(), cb)
902 .unwrap();
903
904 registry.dispatch(
905 old_base,
906 vec![raw("/old/src/a.rs", WatchEventKind::Created, false)],
907 );
908 assert_eq!(wait_events(&collected, 1).len(), 1);
909
910 registry.rebase(new_base);
911 assert!(registry.contains(id));
912 registry.dispatch(
913 old_base,
914 vec![raw("/old/src/stale.rs", WatchEventKind::Created, false)],
915 );
916 registry.dispatch(
917 new_base,
918 vec![raw("/new/src/b.rs", WatchEventKind::Created, false)],
919 );
920
921 let delivered = wait_events(&collected, 2);
922 let paths: Vec<_> = delivered.iter().map(|event| event.path.clone()).collect();
923 assert_eq!(
924 paths,
925 vec![
926 PathBuf::from("/old/src/a.rs"),
927 PathBuf::from("/new/src/b.rs")
928 ]
929 );
930 }
931
932 #[test]
933 fn rebase_from_callback_skips_queued_old_events() {
934 let old_base = Path::new("/old");
935 let new_base = PathBuf::from("/new");
936 let registry = registry(old_base);
937 let collected: Collected = Arc::new(Mutex::new(Vec::new()));
938 let sink = Arc::clone(&collected);
939 let weak = Arc::downgrade(®istry);
940 let callback_base = new_base.clone();
941
942 registry
943 .subscribe(
944 old_base,
945 "**",
946 WatchOptions::default(),
947 Box::new(move |_, events| {
948 sink.lock().extend_from_slice(events);
949 if let Some(registry) = weak.upgrade() {
950 registry.rebase(&callback_base);
951 }
952 }),
953 )
954 .unwrap();
955
956 registry.dispatch(
957 old_base,
958 (0..129)
959 .map(|index| {
960 raw(
961 &format!("/old/file-{index}"),
962 WatchEventKind::Modified,
963 false,
964 )
965 })
966 .collect(),
967 );
968
969 let old_events = wait_events(&collected, 128);
970 assert_eq!(old_events.len(), 128);
971 assert!(
972 !old_events
973 .iter()
974 .any(|event| event.path == Path::new("/old/file-128"))
975 );
976
977 registry.dispatch(
978 &new_base,
979 vec![raw("/new/current", WatchEventKind::Created, false)],
980 );
981 let events = wait_events(&collected, 129);
982 assert_eq!(events.last().unwrap().path, Path::new("/new/current"));
983 }
984
985 #[test]
986 fn callback_panic_does_not_stop_dispatcher() {
987 let base = Path::new("/repo");
988 let registry = registry(base);
989 registry
990 .subscribe(
991 base,
992 "**",
993 WatchOptions::default(),
994 Box::new(|_, _| panic!("test callback panic")),
995 )
996 .unwrap();
997 let (cb, collected) = collector();
998 registry
999 .subscribe(base, "**", WatchOptions::default(), cb)
1000 .unwrap();
1001
1002 registry.dispatch(
1003 base,
1004 vec![raw("/repo/a.rs", WatchEventKind::Created, false)],
1005 );
1006 assert_eq!(wait_events(&collected, 1).len(), 1);
1007 }
1008
1009 #[test]
1010 fn shutdown_and_wait_joins_in_flight_callback() {
1011 let base = Path::new("/repo");
1012 let registry = registry(base);
1013 let (started_tx, started_rx) = mpsc::channel();
1014 let release = Arc::new((Mutex::new(false), Condvar::new()));
1015 let release_cb = Arc::clone(&release);
1016 registry
1017 .subscribe(
1018 base,
1019 "**",
1020 WatchOptions::default(),
1021 Box::new(move |_, _| {
1022 let _ = started_tx.send(());
1023 let (released, ready) = &*release_cb;
1024 ready.wait_while(&mut released.lock(), |released| !*released);
1025 }),
1026 )
1027 .unwrap();
1028 registry.dispatch(
1029 base,
1030 vec![raw("/repo/a.rs", WatchEventKind::Created, false)],
1031 );
1032 started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1033
1034 let registry_wait = Arc::clone(®istry);
1035 let waiting = std::thread::spawn(move || registry_wait.shutdown_and_wait());
1036 std::thread::sleep(Duration::from_millis(20));
1037 assert!(!waiting.is_finished());
1038
1039 let (released, ready) = &*release;
1040 *released.lock() = true;
1041 ready.notify_all();
1042 waiting.join().unwrap();
1043 assert!(!registry.is_active());
1044 }
1045
1046 #[test]
1047 fn index_ignored_events_are_never_delivered() {
1048 let base = Path::new("/repo");
1049 let registry = registry(base);
1050
1051 let (cb, events) = collector();
1052 registry
1053 .subscribe(base, "dist/**", WatchOptions::default(), cb)
1054 .unwrap();
1055
1056 registry.dispatch(
1057 base,
1058 vec![
1059 raw("/repo/dist/bundle.js", WatchEventKind::Created, true),
1060 raw("/repo/dist/keep.js", WatchEventKind::Created, false),
1061 ],
1062 );
1063
1064 let got = wait_events(&events, 1);
1065 assert_eq!(got.len(), 1);
1066 assert_eq!(got[0].path, PathBuf::from("/repo/dist/keep.js"));
1067 }
1068
1069 #[test]
1070 fn callback_receives_its_subscription_id_and_ids_are_unique() {
1071 let base = Path::new("/repo");
1072 let a = registry(base);
1073 let b = registry(base);
1074
1075 let seen_id = Arc::new(Mutex::new(None::<WatchId>));
1076 let seen_cb = Arc::clone(&seen_id);
1077 let id_a = a
1078 .subscribe(
1079 base,
1080 "**",
1081 WatchOptions::default(),
1082 Box::new(move |id, _| {
1083 *seen_cb.lock() = Some(id);
1084 }),
1085 )
1086 .unwrap();
1087 let (b_cb, _b_events) = collector();
1088 let id_b = b
1089 .subscribe(base, "**", WatchOptions::default(), b_cb)
1090 .unwrap();
1091
1092 assert_ne!(id_a, id_b);
1094
1095 a.dispatch(
1096 base,
1097 vec![raw("/repo/a.rs", WatchEventKind::Modified, false)],
1098 );
1099 for _ in 0..200 {
1100 if seen_id.lock().is_some() {
1101 break;
1102 }
1103 std::thread::sleep(Duration::from_millis(5));
1104 }
1105 assert_eq!(*seen_id.lock(), Some(id_a));
1106 }
1107
1108 #[test]
1109 fn shutdown_quiesces_and_allows_restart() {
1110 let base = Path::new("/repo");
1111 let registry = registry(base);
1112 let calls = Arc::new(AtomicUsize::new(0));
1113
1114 let calls_cb = Arc::clone(&calls);
1115 registry
1116 .subscribe(
1117 base,
1118 "**",
1119 WatchOptions::default(),
1120 Box::new(move |_, _| {
1121 calls_cb.fetch_add(1, Ordering::SeqCst);
1122 }),
1123 )
1124 .unwrap();
1125
1126 registry.shutdown();
1127 assert!(!registry.is_active());
1128
1129 registry.dispatch(
1131 base,
1132 vec![raw("/repo/a.rs", WatchEventKind::Modified, false)],
1133 );
1134 std::thread::sleep(std::time::Duration::from_millis(100));
1135 assert_eq!(calls.load(Ordering::SeqCst), 0);
1136
1137 registry.shutdown();
1139 let (cb, events) = collector();
1140 registry
1141 .subscribe(base, "**", WatchOptions::default(), cb)
1142 .unwrap();
1143 registry.dispatch(
1144 base,
1145 vec![raw("/repo/b.rs", WatchEventKind::Created, false)],
1146 );
1147 assert_eq!(wait_events(&events, 1).len(), 1);
1148 }
1149
1150 #[test]
1151 fn unsubscribe_from_inside_callback_does_not_deadlock() {
1152 let base = Path::new("/repo");
1153 let registry = registry(base);
1154 let unsubscribed = Arc::new(AtomicBool::new(false));
1155
1156 let registry_cb = Arc::downgrade(®istry);
1157 let unsub_cb = Arc::clone(&unsubscribed);
1158 registry
1160 .subscribe(
1161 base,
1162 "**",
1163 WatchOptions::default(),
1164 Box::new(move |id, _| {
1165 if let Some(registry) = registry_cb.upgrade() {
1166 registry.unsubscribe(id);
1167 unsub_cb.store(true, Ordering::SeqCst);
1168 }
1169 }),
1170 )
1171 .unwrap();
1172
1173 registry.dispatch(
1174 base,
1175 vec![raw("/repo/a.rs", WatchEventKind::Modified, false)],
1176 );
1177
1178 for _ in 0..400 {
1179 if unsubscribed.load(Ordering::SeqCst) {
1180 break;
1181 }
1182 std::thread::sleep(Duration::from_millis(5));
1183 }
1184 assert!(
1185 unsubscribed.load(Ordering::SeqCst),
1186 "self-unsubscribe from the callback deadlocked"
1187 );
1188 assert!(!registry.is_active());
1189 }
1190}