Skip to main content

codex_file_watcher/
lib.rs

1//! Watches subscribed files or directories and routes coarse-grained change
2//! notifications to the subscribers that own matching watched paths.
3
4use std::collections::BTreeSet;
5use std::collections::HashMap;
6use std::path::Path;
7use std::path::PathBuf;
8use std::sync::Arc;
9use std::sync::Mutex;
10use std::sync::RwLock;
11use std::sync::atomic::AtomicUsize;
12use std::sync::atomic::Ordering;
13use std::time::Duration;
14
15use notify::Event;
16use notify::EventKind;
17use notify::RecommendedWatcher;
18use notify::RecursiveMode;
19use notify::Watcher;
20use tokio::runtime::Handle;
21use tokio::sync::Mutex as AsyncMutex;
22use tokio::sync::Notify;
23use tokio::sync::mpsc;
24use tokio::time::Instant;
25use tokio::time::sleep_until;
26use tracing::warn;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29/// Coalesced file change notification for a subscriber.
30pub struct FileWatcherEvent {
31    /// Changed paths delivered in sorted order with duplicates removed.
32    pub paths: Vec<PathBuf>,
33}
34
35#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36/// Path subscription registered by a [`FileWatcherSubscriber`].
37pub struct WatchPath {
38    /// Root path to watch.
39    pub path: PathBuf,
40    /// Whether events below `path` should match recursively.
41    pub recursive: bool,
42}
43
44type SubscriberId = u64;
45
46#[derive(Default)]
47struct WatchState {
48    next_subscriber_id: SubscriberId,
49    path_ref_counts: HashMap<PathBuf, PathWatchCounts>,
50    subscribers: HashMap<SubscriberId, SubscriberState>,
51}
52
53struct SubscriberState {
54    watched_paths: HashMap<SubscriberWatchKey, SubscriberWatchState>,
55    tx: WatchSender,
56}
57
58/// Immutable per-subscriber watch identity.
59#[derive(Clone, Debug, Eq, Hash, PartialEq)]
60struct SubscriberWatchKey {
61    /// Original path requested by the subscriber. Notifications are reported
62    /// in this namespace so clients do not see canonicalization artifacts.
63    requested: WatchPath,
64    /// Canonical equivalent of `requested` used to match backend events.
65    /// Some backends report canonical paths such as `/private/var/...` even
66    /// when the watch was registered through `/var/...`.
67    matched: WatchPath,
68}
69
70/// Mutable per-subscriber watch state.
71struct SubscriberWatchState {
72    /// Existing path passed to the OS watcher and used for ref-counting. This
73    /// is usually `requested`, but missing targets use an existing ancestor.
74    actual: WatchPath,
75    count: usize,
76    /// Whether the requested path existed the last time an ancestor event was
77    /// handled. This preserves delete notifications for fallback watches.
78    last_exists: bool,
79    /// Whether this watch started from a missing path. Such watches normalize
80    /// ancestor create/delete events back to `requested`.
81    fallback: bool,
82}
83
84/// Registration-time watch data before it is merged into subscriber state.
85///
86/// The key is stable for unregistering while `actual` may later move closer
87/// to the requested path as missing path components are created.
88#[derive(Clone)]
89struct SubscriberWatchRegistration {
90    /// Immutable subscriber-visible identity for this registration.
91    key: SubscriberWatchKey,
92    /// Existing path initially passed to the OS watcher.
93    actual: WatchPath,
94    /// Whether registration started from a missing path fallback.
95    fallback: bool,
96}
97
98/// Receives coalesced change notifications for a single subscriber.
99pub struct Receiver {
100    inner: Arc<ReceiverInner>,
101}
102
103struct WatchSender {
104    inner: Arc<ReceiverInner>,
105}
106
107struct ReceiverInner {
108    changed_paths: AsyncMutex<BTreeSet<PathBuf>>,
109    notify: Notify,
110    sender_count: AtomicUsize,
111}
112
113impl Receiver {
114    /// Waits for the next batch of changed paths, or returns `None` once the
115    /// corresponding subscriber has been removed and no more events can arrive.
116    pub async fn recv(&mut self) -> Option<FileWatcherEvent> {
117        loop {
118            let notified = self.inner.notify.notified();
119            {
120                let mut changed_paths = self.inner.changed_paths.lock().await;
121                if !changed_paths.is_empty() {
122                    return Some(FileWatcherEvent {
123                        paths: std::mem::take(&mut *changed_paths).into_iter().collect(),
124                    });
125                }
126                if self.inner.sender_count.load(Ordering::Acquire) == 0 {
127                    return None;
128                }
129            }
130            notified.await;
131        }
132    }
133}
134
135impl WatchSender {
136    async fn add_changed_paths(&self, paths: &[PathBuf]) {
137        if paths.is_empty() {
138            return;
139        }
140
141        let mut changed_paths = self.inner.changed_paths.lock().await;
142        let previous_len = changed_paths.len();
143        changed_paths.extend(paths.iter().cloned());
144        if changed_paths.len() != previous_len {
145            self.inner.notify.notify_one();
146        }
147    }
148}
149
150impl Clone for WatchSender {
151    fn clone(&self) -> Self {
152        self.inner.sender_count.fetch_add(1, Ordering::Relaxed);
153        Self {
154            inner: Arc::clone(&self.inner),
155        }
156    }
157}
158
159impl Drop for WatchSender {
160    fn drop(&mut self) {
161        if self.inner.sender_count.fetch_sub(1, Ordering::AcqRel) == 1 {
162            self.inner.notify.notify_waiters();
163        }
164    }
165}
166
167fn watch_channel() -> (WatchSender, Receiver) {
168    let inner = Arc::new(ReceiverInner {
169        changed_paths: AsyncMutex::new(BTreeSet::new()),
170        notify: Notify::new(),
171        sender_count: AtomicUsize::new(1),
172    });
173    (
174        WatchSender {
175            inner: Arc::clone(&inner),
176        },
177        Receiver { inner },
178    )
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
182struct PathWatchCounts {
183    non_recursive: usize,
184    recursive: usize,
185}
186
187impl PathWatchCounts {
188    fn increment(&mut self, recursive: bool, amount: usize) {
189        if recursive {
190            self.recursive += amount;
191        } else {
192            self.non_recursive += amount;
193        }
194    }
195
196    fn decrement(&mut self, recursive: bool, amount: usize) {
197        if recursive {
198            self.recursive = self.recursive.saturating_sub(amount);
199        } else {
200            self.non_recursive = self.non_recursive.saturating_sub(amount);
201        }
202    }
203
204    fn effective_mode(self) -> Option<RecursiveMode> {
205        if self.recursive > 0 {
206            Some(RecursiveMode::Recursive)
207        } else if self.non_recursive > 0 {
208            Some(RecursiveMode::NonRecursive)
209        } else {
210            None
211        }
212    }
213
214    fn is_empty(self) -> bool {
215        self.non_recursive == 0 && self.recursive == 0
216    }
217}
218
219struct FileWatcherInner {
220    watcher: RecommendedWatcher,
221    watched_paths: HashMap<PathBuf, RecursiveMode>,
222}
223
224/// Coalesces bursts of watch notifications and emits at most once per interval.
225pub struct ThrottledWatchReceiver {
226    rx: Receiver,
227    interval: Duration,
228    next_allowed: Option<Instant>,
229}
230
231impl ThrottledWatchReceiver {
232    /// Creates a throttling wrapper around a raw watcher [`Receiver`].
233    pub fn new(rx: Receiver, interval: Duration) -> Self {
234        Self {
235            rx,
236            interval,
237            next_allowed: None,
238        }
239    }
240
241    /// Receives the next event, enforcing the configured minimum delay after
242    /// the previous emission.
243    pub async fn recv(&mut self) -> Option<FileWatcherEvent> {
244        if let Some(next_allowed) = self.next_allowed {
245            sleep_until(next_allowed).await;
246        }
247
248        let event = self.rx.recv().await;
249        if event.is_some() {
250            self.next_allowed = Some(Instant::now() + self.interval);
251        }
252        event
253    }
254}
255
256/// Coalesces file watcher notifications that arrive within a fixed debounce
257/// window after the first event in each batch.
258pub struct DebouncedWatchReceiver {
259    rx: Receiver,
260    interval: Duration,
261    changed_paths: BTreeSet<PathBuf>,
262}
263
264impl DebouncedWatchReceiver {
265    /// Creates a debouncing wrapper around a raw watcher [`Receiver`].
266    pub fn new(rx: Receiver, interval: Duration) -> Self {
267        Self {
268            rx,
269            interval,
270            changed_paths: BTreeSet::new(),
271        }
272    }
273
274    /// Receives the next debounced event batch.
275    pub async fn recv(&mut self) -> Option<FileWatcherEvent> {
276        while self.changed_paths.is_empty() {
277            self.changed_paths.extend(self.rx.recv().await?.paths);
278        }
279        let deadline = Instant::now() + self.interval;
280
281        loop {
282            tokio::select! {
283                event = self.rx.recv() => match event {
284                    Some(event) => self.changed_paths.extend(event.paths),
285                    None => break,
286                },
287                _ = sleep_until(deadline) => break,
288            }
289        }
290
291        Some(FileWatcherEvent {
292            paths: std::mem::take(&mut self.changed_paths)
293                .into_iter()
294                .collect(),
295        })
296    }
297}
298
299/// Handle used to register watched paths for one logical consumer.
300pub struct FileWatcherSubscriber {
301    id: SubscriberId,
302    file_watcher: Arc<FileWatcher>,
303}
304
305impl FileWatcherSubscriber {
306    /// Registers the provided paths for this subscriber and returns an RAII
307    /// guard that unregisters them on drop.
308    pub fn register_paths(&self, watched_paths: Vec<WatchPath>) -> WatchRegistration {
309        let watched_paths = dedupe_watched_paths(watched_paths)
310            .into_iter()
311            .map(|requested| {
312                let (actual, matched, fallback) = actual_watch_path(&requested);
313                let key = SubscriberWatchKey { requested, matched };
314                SubscriberWatchRegistration {
315                    key,
316                    actual,
317                    fallback,
318                }
319            })
320            .collect::<Vec<_>>();
321        self.file_watcher.register_paths(self.id, &watched_paths);
322
323        WatchRegistration {
324            file_watcher: Arc::downgrade(&self.file_watcher),
325            subscriber_id: self.id,
326            watched_paths: watched_paths
327                .iter()
328                .map(|watch| watch.key.clone())
329                .collect(),
330        }
331    }
332
333    #[cfg(test)]
334    pub(crate) fn register_path(&self, path: PathBuf, recursive: bool) -> WatchRegistration {
335        self.register_paths(vec![WatchPath { path, recursive }])
336    }
337}
338
339impl Drop for FileWatcherSubscriber {
340    fn drop(&mut self) {
341        self.file_watcher.remove_subscriber(self.id);
342    }
343}
344
345/// RAII guard for a set of active path registrations.
346pub struct WatchRegistration {
347    file_watcher: std::sync::Weak<FileWatcher>,
348    subscriber_id: SubscriberId,
349    watched_paths: Vec<SubscriberWatchKey>,
350}
351
352impl Default for WatchRegistration {
353    fn default() -> Self {
354        Self {
355            file_watcher: std::sync::Weak::new(),
356            subscriber_id: 0,
357            watched_paths: Vec::new(),
358        }
359    }
360}
361
362impl Drop for WatchRegistration {
363    fn drop(&mut self) {
364        if let Some(file_watcher) = self.file_watcher.upgrade() {
365            file_watcher.unregister_paths(self.subscriber_id, &self.watched_paths);
366        }
367    }
368}
369
370/// Multi-subscriber file watcher built on top of `notify`.
371pub struct FileWatcher {
372    inner: Option<Arc<Mutex<FileWatcherInner>>>,
373    state: Arc<RwLock<WatchState>>,
374}
375
376impl FileWatcher {
377    /// Creates a live filesystem watcher and starts its background event loop
378    /// on the current Tokio runtime.
379    pub fn new() -> notify::Result<Self> {
380        let (raw_tx, raw_rx) = mpsc::unbounded_channel();
381        let raw_tx_clone = raw_tx;
382        let watcher = notify::recommended_watcher(move |res| {
383            let _ = raw_tx_clone.send(res);
384        })?;
385        let inner = FileWatcherInner {
386            watcher,
387            watched_paths: HashMap::new(),
388        };
389        let state = Arc::new(RwLock::new(WatchState::default()));
390        let file_watcher = Self {
391            inner: Some(Arc::new(Mutex::new(inner))),
392            state,
393        };
394        file_watcher.spawn_event_loop(raw_rx);
395        Ok(file_watcher)
396    }
397
398    /// Creates an inert watcher that only supports test-driven synthetic
399    /// notifications.
400    pub fn noop() -> Self {
401        Self {
402            inner: None,
403            state: Arc::new(RwLock::new(WatchState::default())),
404        }
405    }
406
407    /// Adds a new subscriber and returns both its registration handle and its
408    /// dedicated event receiver.
409    pub fn add_subscriber(self: &Arc<Self>) -> (FileWatcherSubscriber, Receiver) {
410        let (tx, rx) = watch_channel();
411        let mut state = self
412            .state
413            .write()
414            .unwrap_or_else(std::sync::PoisonError::into_inner);
415        let subscriber_id = state.next_subscriber_id;
416        state.next_subscriber_id += 1;
417        state.subscribers.insert(
418            subscriber_id,
419            SubscriberState {
420                watched_paths: HashMap::new(),
421                tx,
422            },
423        );
424
425        let subscriber = FileWatcherSubscriber {
426            id: subscriber_id,
427            file_watcher: self.clone(),
428        };
429        (subscriber, rx)
430    }
431
432    fn register_paths(
433        &self,
434        subscriber_id: SubscriberId,
435        watched_paths: &[SubscriberWatchRegistration],
436    ) {
437        let mut state = self
438            .state
439            .write()
440            .unwrap_or_else(std::sync::PoisonError::into_inner);
441        let mut inner_guard: Option<std::sync::MutexGuard<'_, FileWatcherInner>> = None;
442
443        for registration in watched_paths {
444            let actual = {
445                let Some(subscriber) = state.subscribers.get_mut(&subscriber_id) else {
446                    return;
447                };
448                match subscriber.watched_paths.entry(registration.key.clone()) {
449                    std::collections::hash_map::Entry::Occupied(mut entry) => {
450                        entry.get_mut().count += 1;
451                        entry.get().actual.clone()
452                    }
453                    std::collections::hash_map::Entry::Vacant(entry) => {
454                        entry.insert(SubscriberWatchState {
455                            actual: registration.actual.clone(),
456                            count: 1,
457                            last_exists: registration.key.matched.path.exists(),
458                            fallback: registration.fallback,
459                        });
460                        registration.actual.clone()
461                    }
462                }
463            };
464
465            let counts = state
466                .path_ref_counts
467                .entry(actual.path.clone())
468                .or_default();
469            let previous_mode = counts.effective_mode();
470            counts.increment(actual.recursive, /*amount*/ 1);
471            let next_mode = counts.effective_mode();
472            if previous_mode != next_mode {
473                self.reconfigure_watch(&actual.path, next_mode, &mut inner_guard);
474            }
475        }
476    }
477
478    fn unregister_paths(&self, subscriber_id: SubscriberId, watched_paths: &[SubscriberWatchKey]) {
479        let mut state = self
480            .state
481            .write()
482            .unwrap_or_else(std::sync::PoisonError::into_inner);
483        let mut inner_guard: Option<std::sync::MutexGuard<'_, FileWatcherInner>> = None;
484
485        for subscriber_watch in watched_paths {
486            let actual = {
487                let Some(subscriber) = state.subscribers.get_mut(&subscriber_id) else {
488                    return;
489                };
490                let Some(subscriber_watch_state) =
491                    subscriber.watched_paths.get_mut(subscriber_watch)
492                else {
493                    continue;
494                };
495                let actual = subscriber_watch_state.actual.clone();
496                subscriber_watch_state.count = subscriber_watch_state.count.saturating_sub(1);
497                if subscriber_watch_state.count == 0 {
498                    subscriber.watched_paths.remove(subscriber_watch);
499                }
500                actual
501            };
502
503            let Some(counts) = state.path_ref_counts.get_mut(&actual.path) else {
504                continue;
505            };
506            let previous_mode = counts.effective_mode();
507            counts.decrement(actual.recursive, /*amount*/ 1);
508            let next_mode = counts.effective_mode();
509            if counts.is_empty() {
510                state.path_ref_counts.remove(&actual.path);
511            }
512            if previous_mode != next_mode {
513                self.reconfigure_watch(&actual.path, next_mode, &mut inner_guard);
514            }
515        }
516    }
517
518    fn remove_subscriber(&self, subscriber_id: SubscriberId) {
519        let mut state = self
520            .state
521            .write()
522            .unwrap_or_else(std::sync::PoisonError::into_inner);
523        let Some(subscriber) = state.subscribers.remove(&subscriber_id) else {
524            return;
525        };
526
527        let mut inner_guard: Option<std::sync::MutexGuard<'_, FileWatcherInner>> = None;
528        for (_subscriber_watch, subscriber_watch_state) in subscriber.watched_paths {
529            let Some(path_counts) = state
530                .path_ref_counts
531                .get_mut(&subscriber_watch_state.actual.path)
532            else {
533                continue;
534            };
535            let previous_mode = path_counts.effective_mode();
536            path_counts.decrement(
537                subscriber_watch_state.actual.recursive,
538                subscriber_watch_state.count,
539            );
540            let next_mode = path_counts.effective_mode();
541            if path_counts.is_empty() {
542                state
543                    .path_ref_counts
544                    .remove(&subscriber_watch_state.actual.path);
545            }
546            if previous_mode != next_mode {
547                self.reconfigure_watch(
548                    &subscriber_watch_state.actual.path,
549                    next_mode,
550                    &mut inner_guard,
551                );
552            }
553        }
554    }
555
556    fn reconfigure_watch<'a>(
557        &'a self,
558        path: &Path,
559        next_mode: Option<RecursiveMode>,
560        inner_guard: &mut Option<std::sync::MutexGuard<'a, FileWatcherInner>>,
561    ) {
562        Self::reconfigure_watch_inner(self.inner.as_ref(), path, next_mode, inner_guard);
563    }
564
565    fn reconfigure_watch_inner<'a>(
566        inner: Option<&'a Arc<Mutex<FileWatcherInner>>>,
567        path: &Path,
568        next_mode: Option<RecursiveMode>,
569        inner_guard: &mut Option<std::sync::MutexGuard<'a, FileWatcherInner>>,
570    ) {
571        let Some(inner) = inner else {
572            return;
573        };
574        if inner_guard.is_none() {
575            let guard = inner
576                .lock()
577                .unwrap_or_else(std::sync::PoisonError::into_inner);
578            *inner_guard = Some(guard);
579        }
580        let Some(guard) = inner_guard.as_mut() else {
581            return;
582        };
583
584        let existing_mode = guard.watched_paths.get(path).copied();
585        if existing_mode == next_mode {
586            return;
587        }
588
589        if existing_mode.is_some() {
590            if let Err(err) = guard.watcher.unwatch(path) {
591                warn!("failed to unwatch {}: {err}", path.display());
592            }
593            guard.watched_paths.remove(path);
594        }
595
596        let Some(next_mode) = next_mode else {
597            return;
598        };
599        if !path.exists() {
600            return;
601        }
602
603        if let Err(err) = guard.watcher.watch(path, next_mode) {
604            warn!("failed to watch {}: {err}", path.display());
605            return;
606        }
607        guard.watched_paths.insert(path.to_path_buf(), next_mode);
608    }
609
610    fn apply_actual_watch_move<'a>(
611        path_ref_counts: &mut HashMap<PathBuf, PathWatchCounts>,
612        old_actual: WatchPath,
613        new_actual: WatchPath,
614        count: usize,
615        inner: Option<&'a Arc<Mutex<FileWatcherInner>>>,
616        inner_guard: &mut Option<std::sync::MutexGuard<'a, FileWatcherInner>>,
617    ) {
618        if old_actual == new_actual {
619            return;
620        }
621
622        if let Some(counts) = path_ref_counts.get_mut(&old_actual.path) {
623            let previous_mode = counts.effective_mode();
624            counts.decrement(old_actual.recursive, count);
625            let next_mode = counts.effective_mode();
626            if counts.is_empty() {
627                path_ref_counts.remove(&old_actual.path);
628            }
629            if previous_mode != next_mode {
630                Self::reconfigure_watch_inner(inner, &old_actual.path, next_mode, inner_guard);
631            }
632        }
633
634        let counts = path_ref_counts.entry(new_actual.path.clone()).or_default();
635        let previous_mode = counts.effective_mode();
636        counts.increment(new_actual.recursive, count);
637        let next_mode = counts.effective_mode();
638        if previous_mode != next_mode {
639            Self::reconfigure_watch_inner(inner, &new_actual.path, next_mode, inner_guard);
640        }
641    }
642
643    // Bridge `notify`'s callback-based events into the Tokio runtime and
644    // notify the matching subscribers.
645    fn spawn_event_loop(&self, mut raw_rx: mpsc::UnboundedReceiver<notify::Result<Event>>) {
646        if let Ok(handle) = Handle::try_current() {
647            let state = Arc::clone(&self.state);
648            let inner = self.inner.as_ref().map(Arc::downgrade);
649            handle.spawn(async move {
650                loop {
651                    match raw_rx.recv().await {
652                        Some(Ok(event)) => {
653                            if !is_mutating_event(&event) {
654                                continue;
655                            }
656                            if event.paths.is_empty() {
657                                continue;
658                            }
659                            let inner = inner.as_ref().and_then(std::sync::Weak::upgrade);
660                            Self::notify_subscribers(&state, inner.as_ref(), &event.paths).await;
661                        }
662                        Some(Err(err)) => {
663                            warn!("file watcher error: {err}");
664                        }
665                        None => break,
666                    }
667                }
668            });
669        } else {
670            warn!("file watcher loop skipped: no Tokio runtime available");
671        }
672    }
673
674    async fn notify_subscribers(
675        state: &RwLock<WatchState>,
676        inner: Option<&Arc<Mutex<FileWatcherInner>>>,
677        event_paths: &[PathBuf],
678    ) {
679        let subscribers_to_notify: Vec<(WatchSender, Vec<PathBuf>)> = {
680            let mut state = state
681                .write()
682                .unwrap_or_else(std::sync::PoisonError::into_inner);
683            let mut actual_watch_moves = Vec::new();
684            let mut subscribers_to_notify = Vec::new();
685
686            for subscriber in state.subscribers.values_mut() {
687                let mut changed_paths = Vec::new();
688                for event_path in event_paths {
689                    for (subscriber_watch, subscriber_watch_state) in &mut subscriber.watched_paths
690                    {
691                        if let Some(path) = changed_path_for_event(
692                            subscriber_watch,
693                            subscriber_watch_state,
694                            event_path,
695                        ) {
696                            changed_paths.push(path);
697                        }
698
699                        let (new_actual, _new_matched, fallback) =
700                            actual_watch_path(&subscriber_watch.requested);
701                        subscriber_watch_state.fallback |= fallback;
702                        if subscriber_watch_state.actual != new_actual {
703                            let old_actual = subscriber_watch_state.actual.clone();
704                            let count = subscriber_watch_state.count;
705                            subscriber_watch_state.actual = new_actual.clone();
706                            actual_watch_moves.push((old_actual, new_actual, count));
707                        }
708                    }
709                }
710                if !changed_paths.is_empty() {
711                    subscribers_to_notify.push((subscriber.tx.clone(), changed_paths));
712                }
713            }
714
715            let mut inner_guard: Option<std::sync::MutexGuard<'_, FileWatcherInner>> = None;
716            for (old_actual, new_actual, count) in actual_watch_moves {
717                Self::apply_actual_watch_move(
718                    &mut state.path_ref_counts,
719                    old_actual,
720                    new_actual,
721                    count,
722                    inner,
723                    &mut inner_guard,
724                );
725            }
726
727            subscribers_to_notify
728        };
729
730        for (subscriber, changed_paths) in subscribers_to_notify {
731            subscriber.add_changed_paths(&changed_paths).await;
732        }
733    }
734
735    #[cfg(test)]
736    pub(crate) async fn send_paths_for_test(&self, paths: Vec<PathBuf>) {
737        Self::notify_subscribers(&self.state, self.inner.as_ref(), &paths).await;
738    }
739
740    #[cfg(test)]
741    pub(crate) fn spawn_event_loop_for_test(
742        &self,
743        raw_rx: mpsc::UnboundedReceiver<notify::Result<Event>>,
744    ) {
745        self.spawn_event_loop(raw_rx);
746    }
747
748    #[cfg(test)]
749    pub(crate) fn watch_counts_for_test(&self, path: &Path) -> Option<(usize, usize)> {
750        let state = self
751            .state
752            .read()
753            .unwrap_or_else(std::sync::PoisonError::into_inner);
754        state
755            .path_ref_counts
756            .get(path)
757            .map(|counts| (counts.non_recursive, counts.recursive))
758    }
759}
760
761fn is_mutating_event(event: &Event) -> bool {
762    matches!(
763        event.kind,
764        EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
765    )
766}
767
768fn dedupe_watched_paths(mut watched_paths: Vec<WatchPath>) -> Vec<WatchPath> {
769    watched_paths.sort_unstable_by(|a, b| {
770        a.path
771            .as_os_str()
772            .cmp(b.path.as_os_str())
773            .then(a.recursive.cmp(&b.recursive))
774    });
775    watched_paths.dedup();
776    watched_paths
777}
778
779/// Returns the actual OS watch path and canonical match path for a request.
780///
781/// Missing targets are watched non-recursively through the nearest existing
782/// directory ancestor. As path components appear, the actual watch is moved
783/// closer to the requested path so broad recursive ancestor watches are never
784/// needed.
785fn actual_watch_path(requested: &WatchPath) -> (WatchPath, WatchPath, bool) {
786    if requested.path.exists() {
787        let matched_path = requested
788            .path
789            .canonicalize()
790            .unwrap_or_else(|_| requested.path.clone());
791        let actual = requested.clone();
792        let matched = WatchPath {
793            path: matched_path,
794            recursive: requested.recursive,
795        };
796        return (actual, matched, false);
797    }
798
799    let requested_parent = requested.path.parent();
800    let mut ancestor = requested_parent;
801    while let Some(path) = ancestor {
802        if path.is_dir() {
803            let actual_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
804            let matched_path = requested
805                .path
806                .strip_prefix(path)
807                .map(|suffix| actual_path.join(suffix))
808                .unwrap_or_else(|_| requested.path.clone());
809            let actual = WatchPath {
810                path: path.to_path_buf(),
811                recursive: false,
812            };
813            let matched = WatchPath {
814                path: matched_path,
815                recursive: requested.recursive,
816            };
817            return (actual, matched, true);
818        }
819        ancestor = path.parent();
820    }
821
822    (requested.clone(), requested.clone(), false)
823}
824
825/// Converts one raw backend event path into the subscriber-visible path.
826///
827/// Matching first uses the canonical path namespace reported by many OS
828/// backends, then falls back to the originally requested namespace for
829/// synthetic tests and backends that preserve the input spelling.
830fn changed_path_for_event(
831    subscriber_watch: &SubscriberWatchKey,
832    subscriber_watch_state: &mut SubscriberWatchState,
833    event_path: &Path,
834) -> Option<PathBuf> {
835    if let Some(path) = changed_path_for_matched_path(
836        subscriber_watch,
837        subscriber_watch_state,
838        &subscriber_watch.matched,
839        event_path,
840    ) {
841        return Some(path);
842    }
843    if subscriber_watch.matched.path == subscriber_watch.requested.path {
844        return None;
845    }
846    changed_path_for_matched_path(
847        subscriber_watch,
848        subscriber_watch_state,
849        &subscriber_watch.requested,
850        event_path,
851    )
852}
853
854/// Applies the watch matching rules in one path namespace and maps any emitted
855/// path back into the subscriber's requested namespace.
856fn changed_path_for_matched_path(
857    subscriber_watch: &SubscriberWatchKey,
858    subscriber_watch_state: &mut SubscriberWatchState,
859    matched: &WatchPath,
860    event_path: &Path,
861) -> Option<PathBuf> {
862    let requested = &subscriber_watch.requested;
863    if event_path == matched.path {
864        subscriber_watch_state.last_exists = matched.path.exists();
865        return Some(requested.path.clone());
866    }
867    if matched.path.starts_with(event_path) {
868        let now_exists = matched.path.exists();
869        if subscriber_watch_state.fallback {
870            let should_notify = now_exists || subscriber_watch_state.last_exists;
871            subscriber_watch_state.last_exists = now_exists;
872            return should_notify.then(|| requested.path.clone());
873        }
874        if subscriber_watch_state.actual.path != matched.path {
875            let should_notify = now_exists || subscriber_watch_state.last_exists;
876            subscriber_watch_state.last_exists = now_exists;
877            return should_notify.then(|| requested.path.clone());
878        }
879        subscriber_watch_state.last_exists = now_exists;
880        return Some(event_path.to_path_buf());
881    }
882    if !event_path.starts_with(&matched.path) {
883        return None;
884    }
885    if !(matched.recursive || event_path.parent() == Some(matched.path.as_path())) {
886        return None;
887    }
888    subscriber_watch_state.last_exists = matched.path.exists();
889    Some(
890        event_path
891            .strip_prefix(&matched.path)
892            .map(|suffix| requested.path.join(suffix))
893            .unwrap_or_else(|_| event_path.to_path_buf()),
894    )
895}
896
897#[cfg(test)]
898#[path = "file_watcher_tests.rs"]
899mod tests;