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