Skip to main content

notify/
lib.rs

1//! Cross-platform file system notification library
2//!
3//! # Installation
4//!
5//! ```toml
6//! [dependencies]
7//! notify = "9.0.0-rc.6"
8//! ```
9//!
10//! If you want debounced events (or don't need them in-order), see [notify-debouncer-mini](https://docs.rs/notify-debouncer-mini/latest/notify_debouncer_mini/)
11//! or [notify-debouncer-full](https://docs.rs/notify-debouncer-full/latest/notify_debouncer_full/).
12//!
13//! ## Features
14//!
15//! List of compilation features, see below for details
16//!
17//! - `serde` for serialization of events
18//! - `macos_fsevent` (default) for the FSEvents backend on macOS
19//! - `macos_kqueue` for the kqueue backend on macOS
20//! - `freebsd_inotify` enables notify's inotify backend on FreeBSD 14.5+
21//!   - inotify is automatically enabled when built natively, this feature is only needed for cross-compilation
22//! - `serialization-compat-6` restores the serialization behavior of notify 6, off by default
23//!
24//! Native FreeBSD builds use inotify on 14.5+ and kqueue on older releases. When
25//! cross-compiling for FreeBSD 14.5+, enable `freebsd_inotify` explicitly.
26//!
27//! ### Serde
28//!
29//! Events are serializable via [serde](https://serde.rs) if the `serde` feature is enabled:
30//!
31//! ```toml
32//! notify = { version = "9.0.0-rc.6", features = ["serde"] }
33//! ```
34//!
35//! # Known Problems
36//!
37//! ### Network filesystems
38//!
39//! Network mounted filesystems like NFS may not emit any events for notify to listen to.
40//! This applies especially to WSL programs watching windows paths ([issue #254](https://github.com/notify-rs/notify/issues/254)).
41//!
42//! A workaround is the [`PollWatcher`] backend.
43//!
44//! ### Docker with Linux on macOS M1
45//!
46//! Docker on macOS M1 [throws](https://github.com/notify-rs/notify/issues/423) `Function not implemented (os error 38)`.
47//! You have to manually use the [`PollWatcher`], as the native backend isn't available inside the emulation.
48//!
49//! ### macOS, FSEvents and unowned files
50//!
51//! Due to the inner security model of FSEvents (see [FileSystemEventSecurity](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/FileSystemEventSecurity/FileSystemEventSecurity.html)),
52//! some events cannot be observed easily when trying to follow files that do not
53//! belong to you. In this case, reverting to the pollwatcher can fix the issue,
54//! with a slight performance cost.
55//!
56//! ### macOS/APFS: `std::fs::copy` can trigger events on the source path
57//!
58//! On APFS, `std::fs::copy` may use copy-on-write cloning (`fclonefileat`/`clonefile`).
59//! This can update inode metadata on the source file, and FSEvents may report a metadata change
60//! for the source path (see [issue #259](https://github.com/notify-rs/notify/issues/259) and
61//! [issue #465](https://github.com/notify-rs/notify/issues/465)).
62//!
63//! Workarounds are to avoid `std::fs::copy` (use `std::io::copy` or `read`/`write` instead), or
64//! filter out metadata-only events if they're not relevant (e.g. don't include
65//! `EventKindMask::MODIFY_META` in [`Config::with_event_kinds`]).
66//!
67//! ### Editor Behaviour
68//!
69//! If you rely on precise events (Write/Delete/Create..), you will notice that the actual events
70//! can differ a lot between file editors. Some truncate the file on save, some create a new one and replace the old one.
71//! See also [this](https://github.com/notify-rs/notify/issues/247) and [this](https://github.com/notify-rs/notify/issues/113#issuecomment-281836995) issues for example.
72//!
73//! ### Parent folder deletion
74//!
75//! If you want to receive an event for a deletion of folder `b` for the path `/a/b/..`, you will have to watch its parent `/a`.
76//! See [here](https://github.com/notify-rs/notify/issues/403) for more details.
77//!
78//! ### Pseudo Filesystems like /proc, /sys
79//!
80//! Some filesystems like `/proc` and `/sys` on *nix do not emit change events or use correct file change dates.
81//! To circumvent that problem you can use the [`PollWatcher`] with the `compare_contents` option.
82//!
83//! ### Linux: Bad File Descriptor / No space left on device
84//!
85//! This may be the case of running into the max-files watched limits of your user or system.
86//! (Files also includes folders.) Note that for recursive watched folders each file and folder inside counts towards the limit.
87//!
88//! You may increase this limit in linux via
89//! ```sh
90//! sudo sysctl fs.inotify.max_user_instances=8192 # example number
91//! sudo sysctl fs.inotify.max_user_watches=524288 # example number
92//! sudo sysctl -p
93//! ```
94//!
95//! Note that the [`PollWatcher`] is not restricted by this limitation, so it may be an alternative if your users can't increase the limit.
96//!
97//! ### Watching large directories
98//!
99//! When watching a very large amount of files, notify may fail to receive all events.
100//! For example the linux backend is documented to not be a 100% reliable source. See also issue [#412](https://github.com/notify-rs/notify/issues/412).
101//!
102//! ### Watching many paths on macOS
103//!
104//! Use [`Watcher::update_paths`] to register many paths in one batch. The FSEvents backend
105//! rebuilds its stream when adding a path that is not already covered by a recursive watch on
106//! the same volume. Adding N independent paths with separate [`Watcher::watch`] calls therefore
107//! registers O(N²) paths in total; a single batch rebuilds the stream only once.
108//!
109//! # Examples
110//!
111//! For more examples visit the [examples folder](https://github.com/notify-rs/notify/tree/main/examples) in the repository.
112//!
113//! ```rust
114//! use notify::{Event, RecursiveMode, Result, Watcher};
115//! use std::{path::Path, sync::mpsc};
116//!
117//! fn main() -> Result<()> {
118//!     let (tx, rx) = mpsc::channel::<Result<Event>>();
119//!
120//!     // Use recommended_watcher() to automatically select the best implementation
121//!     // for your platform. The `EventHandler` passed to this constructor can be a
122//!     // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or
123//!     // another type the trait is implemented for.
124//!     let mut watcher = notify::recommended_watcher(tx)?;
125//!
126//!     // Add a path to be watched. All files and directories at that path and
127//!     // below will be monitored for changes.
128//! #     #[cfg(not(any(
129//! #     target_os = "freebsd",
130//! #     target_os = "openbsd",
131//! #     target_os = "dragonfly",
132//! #     target_os = "netbsd")))]
133//! #     { // "." doesn't exist on BSD for some reason in CI
134//!     watcher.watch(Path::new("."), RecursiveMode::Recursive)?;
135//! #     }
136//! #     #[cfg(any())]
137//! #     { // don't run this in doctests, it blocks forever
138//!     // Block forever, printing out events as they come in
139//!     for res in rx {
140//!         match res {
141//!             Ok(event) => println!("event: {:?}", event),
142//!             Err(e) => println!("watch error: {:?}", e),
143//!         }
144//!     }
145//! #     }
146//!
147//!     Ok(())
148//! }
149//! ```
150//!
151//! ## With different configurations
152//!
153//! It is possible to create several watchers with different configurations or implementations that
154//! all call the same event function. This can accommodate advanced behaviour or work around limits.
155//!
156//! ```rust
157//! # use notify::{RecursiveMode, Result, Watcher};
158//! # use std::path::Path;
159//! #
160//! # fn main() -> Result<()> {
161//!       fn event_fn(res: Result<notify::Event>) {
162//!           match res {
163//!              Ok(event) => println!("event: {:?}", event),
164//!              Err(e) => println!("watch error: {:?}", e),
165//!           }
166//!       }
167//!
168//!       let mut watcher1 = notify::recommended_watcher(event_fn)?;
169//!       // we will just use the same watcher kind again here
170//!       let mut watcher2 = notify::recommended_watcher(event_fn)?;
171//! #     #[cfg(not(any(
172//! #     target_os = "freebsd",
173//! #     target_os = "openbsd",
174//! #     target_os = "dragonfly",
175//! #     target_os = "netbsd")))]
176//! #     { // "." doesn't exist on BSD for some reason in CI
177//! #     watcher1.watch(Path::new("."), RecursiveMode::Recursive)?;
178//! #     watcher2.watch(Path::new("."), RecursiveMode::Recursive)?;
179//! #     }
180//!       // dropping the watcher1/2 here (no loop etc) will end the program
181//! #
182//! #     Ok(())
183//! # }
184//! ```
185
186#![deny(missing_docs)]
187
188pub use config::{Config, PathOp, RecursiveMode, WatchPathConfig, WindowsPathSeparatorStyle};
189pub use error::{Error, ErrorKind, Result, UpdatePathsError};
190pub use notify_types::event::{self, Event, EventKind, EventKindMask};
191use std::path::{Path, PathBuf};
192
193pub(crate) type StdResult<T, E> = std::result::Result<T, E>;
194pub(crate) type Receiver<T> = std::sync::mpsc::Receiver<T>;
195pub(crate) type Sender<T> = std::sync::mpsc::Sender<T>;
196#[cfg(any(
197    target_os = "linux",
198    target_os = "android",
199    all(target_os = "freebsd", notify_freebsd_inotify),
200    target_os = "windows",
201))]
202pub(crate) type BoundSender<T> = std::sync::mpsc::SyncSender<T>;
203
204#[inline]
205pub(crate) fn unbounded<T>() -> (Sender<T>, Receiver<T>) {
206    std::sync::mpsc::channel()
207}
208
209#[cfg(any(
210    target_os = "linux",
211    target_os = "android",
212    all(target_os = "freebsd", notify_freebsd_inotify),
213    target_os = "windows",
214))]
215#[inline]
216pub(crate) fn bounded<T>(cap: usize) -> (BoundSender<T>, Receiver<T>) {
217    std::sync::mpsc::sync_channel(cap)
218}
219
220#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
221pub use crate::fsevent::FsEventWatcher;
222#[cfg(any(
223    target_os = "linux",
224    target_os = "android",
225    all(target_os = "freebsd", notify_freebsd_inotify),
226))]
227pub use crate::inotify::INotifyWatcher;
228#[cfg(any(
229    target_os = "freebsd",
230    target_os = "openbsd",
231    target_os = "netbsd",
232    target_os = "dragonfly",
233    target_os = "ios",
234    all(target_os = "macos", feature = "macos_kqueue")
235))]
236pub use crate::kqueue::KqueueWatcher;
237pub use null::NullWatcher;
238pub use poll::PollWatcher;
239#[cfg(target_os = "windows")]
240pub use windows::ReadDirectoryChangesWatcher;
241
242#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
243pub mod fsevent;
244#[cfg(any(
245    target_os = "linux",
246    target_os = "android",
247    all(target_os = "freebsd", notify_freebsd_inotify),
248))]
249pub mod inotify;
250#[cfg(any(
251    target_os = "freebsd",
252    target_os = "openbsd",
253    target_os = "dragonfly",
254    target_os = "netbsd",
255    target_os = "ios",
256    all(target_os = "macos", feature = "macos_kqueue")
257))]
258pub mod kqueue;
259#[cfg(target_os = "windows")]
260pub mod windows;
261
262pub mod null;
263pub mod poll;
264
265mod config;
266mod error;
267mod paths;
268
269#[cfg(test)]
270pub(crate) mod test;
271
272/// The set of requirements for watcher event handling functions.
273///
274/// # Example implementation
275///
276/// ```no_run
277/// use notify::{Event, Result, EventHandler};
278///
279/// /// Prints received events
280/// struct EventPrinter;
281///
282/// impl EventHandler for EventPrinter {
283///     fn handle_event(&mut self, event: Result<Event>) {
284///         if let Ok(event) = event {
285///             println!("Event: {:?}", event);
286///         }
287///     }
288/// }
289/// ```
290pub trait EventHandler: Send + 'static {
291    /// Handles an event.
292    fn handle_event(&mut self, event: Result<Event>);
293}
294
295impl<F> EventHandler for F
296where
297    F: FnMut(Result<Event>) + Send + 'static,
298{
299    fn handle_event(&mut self, event: Result<Event>) {
300        (self)(event);
301    }
302}
303
304#[cfg(feature = "crossbeam-channel")]
305impl EventHandler for crossbeam_channel::Sender<Result<Event>> {
306    fn handle_event(&mut self, event: Result<Event>) {
307        let _ = self.send(event);
308    }
309}
310
311#[cfg(feature = "flume")]
312impl EventHandler for flume::Sender<Result<Event>> {
313    fn handle_event(&mut self, event: Result<Event>) {
314        let _ = self.send(event);
315    }
316}
317
318#[cfg(feature = "futures")]
319impl EventHandler for futures::channel::mpsc::UnboundedSender<Result<Event>> {
320    fn handle_event(&mut self, event: Result<Event>) {
321        let _ = self.unbounded_send(event);
322    }
323}
324
325#[cfg(feature = "tokio")]
326impl EventHandler for tokio::sync::mpsc::UnboundedSender<Result<Event>> {
327    fn handle_event(&mut self, event: Result<Event>) {
328        let _ = self.send(event);
329    }
330}
331
332impl EventHandler for std::sync::mpsc::Sender<Result<Event>> {
333    fn handle_event(&mut self, event: Result<Event>) {
334        let _ = self.send(event);
335    }
336}
337
338/// Watcher kind enumeration
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
340#[non_exhaustive]
341pub enum WatcherKind {
342    /// Inotify backend (Linux, Android, and FreeBSD 14.5+)
343    Inotify,
344    /// FS-Event backend (mac)
345    Fsevent,
346    /// KQueue backend (bsd,optionally mac)
347    Kqueue,
348    /// Polling based backend (fallback)
349    PollWatcher,
350    /// Windows backend
351    ReadDirectoryChangesWatcher,
352    /// Fake watcher for testing
353    NullWatcher,
354}
355
356/// Type that can deliver file activity notifications
357///
358/// `Watcher` is implemented per platform using the best implementation available on that platform.
359/// In addition to such event driven implementations, a polling implementation is also provided
360/// that should work on any platform.
361///
362/// # Creating a watcher
363///
364/// Because `Watcher` is a trait, Rust usually can't infer the concrete watcher type when calling
365/// `Watcher::new(...)` directly. Prefer [`recommended_watcher`] / [`RecommendedWatcher`] (or a
366/// specific backend like [`PollWatcher`]) when constructing a watcher.
367///
368/// ```no_run
369/// use notify::{Event, RecursiveMode, Result, Watcher};
370/// use std::{path::Path, sync::mpsc};
371///
372/// fn main() -> Result<()> {
373///     let (tx, _rx) = mpsc::channel::<Result<Event>>();
374///     let mut watcher = notify::recommended_watcher(tx)?;
375///     watcher.watch(Path::new("."), RecursiveMode::Recursive)?;
376///     Ok(())
377/// }
378/// ```
379pub trait Watcher {
380    /// Create a new watcher with an initial Config.
381    fn new<F: EventHandler>(event_handler: F, config: config::Config) -> Result<Self>
382    where
383        Self: Sized;
384    /// Begin watching a new path.
385    ///
386    /// If the `path` is a directory, `recursive_mode` will be evaluated. If `recursive_mode` is
387    /// `RecursiveMode::Recursive` events will be delivered for all files in that tree. Otherwise
388    /// only the directory and its immediate children will be watched.
389    ///
390    /// If the `path` is a file, `recursive_mode` will be ignored and events will be delivered only
391    /// for the file.
392    ///
393    /// Event paths are reported using the same root representation as `path`. If `path` is
394    /// relative, emitted event paths are relative to the process current directory at the time this
395    /// method is called. If `path` is absolute, emitted event paths are absolute. Convert `path`
396    /// before calling this method if your application needs a specific representation.
397    ///
398    /// On success, calling this method again for the same backend-resolved path replaces the
399    /// existing watch for that path. The recursive mode is updated to the new value, a second
400    /// independent watch is not added, and a single call to [`Watcher::unwatch`] removes the
401    /// watched path.
402    ///
403    /// To add many paths at once, use [`Watcher::update_paths`]. On macOS, this avoids rebuilding
404    /// the FSEvents stream for each path.
405    ///
406    /// On some platforms, if the `path` is renamed or removed while being watched, behaviour may
407    /// be unexpected. See discussions in [#165] and [#166]. If less surprising behaviour is wanted
408    /// one may non-recursively watch the _parent_ directory as well and manage related events.
409    ///
410    /// [#165]: https://github.com/notify-rs/notify/issues/165
411    /// [#166]: https://github.com/notify-rs/notify/issues/166
412    fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
413
414    /// Begin watching a path with per-path settings.
415    ///
416    /// Behaves like [`Watcher::watch`], with the options in `config` applied to this watch alone.
417    /// Backends that do not implement a given option ignore it, so the default implementation
418    /// applies only [`WatchPathConfig::recursive_mode`].
419    ///
420    /// # Errors
421    ///
422    /// Returns an error in the case that `path` does not exist or if adding the watch fails.
423    fn watch_with(&mut self, path: &Path, config: WatchPathConfig) -> Result<()> {
424        self.watch(path, config.recursive_mode())
425    }
426
427    /// Stop watching a path.
428    ///
429    /// # Errors
430    ///
431    /// Returns an error in the case that `path` has not been watched or if removing the watch
432    /// fails.
433    fn unwatch(&mut self, path: &Path) -> Result<()>;
434
435    /// Add/remove paths to watch in batch.
436    ///
437    /// For some [`Watcher`] implementations this method provides better performance than multiple
438    /// calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once.
439    /// In particular, the FSEvents backend on macOS rebuilds its stream only once for the whole batch.
440    ///
441    /// # Errors
442    ///
443    /// Returns [`UpdatePathsError`] if any operation fails. Operations are applied sequentially.
444    /// When an error occurs, processing stops: operations before `origin` have been applied,
445    /// `origin` is the operation that failed (if known), and `remaining` are the operations that
446    /// were not attempted. `remaining` does not include `origin`.
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// # use notify::{Watcher, RecursiveMode, PathOp};
452    /// # use std::path::{Path, PathBuf};
453    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
454    /// # let many_paths_to_add: Vec<PathBuf> = vec![];
455    /// # let many_paths_to_remove: Vec<PathBuf> = vec![];
456    /// let mut watcher = notify::NullWatcher;
457    /// let mut batch = Vec::new();
458    ///
459    /// for path in many_paths_to_add {
460    ///     batch.push(PathOp::watch_recursive(path));
461    /// }
462    ///
463    /// for path in many_paths_to_remove {
464    ///     batch.push(PathOp::unwatch(path));
465    /// }
466    ///
467    /// // real work is done there
468    /// watcher.update_paths(batch)?;
469    /// # Ok(())
470    /// # }
471    /// ```
472    fn update_paths(&mut self, ops: Vec<PathOp>) -> StdResult<(), UpdatePathsError> {
473        update_paths(ops, |op| match op {
474            PathOp::Watch(path, config) => self
475                .watch_with(&path, config.clone())
476                .map_err(|e| (PathOp::Watch(path, config), e)),
477            PathOp::Unwatch(path) => self.unwatch(&path).map_err(|e| (PathOp::Unwatch(path), e)),
478        })
479    }
480
481    /// Configure the watcher at runtime.
482    ///
483    /// See the [`Config`](config/struct.Config.html) struct for all configuration options.
484    ///
485    /// # Returns
486    ///
487    /// - `Ok(true)` on success.
488    /// - `Ok(false)` if the watcher does not support or implement the option.
489    /// - `Err(notify::Error)` on failure.
490    fn configure(&mut self, _option: Config) -> Result<bool> {
491        Ok(false)
492    }
493
494    /// Returns the currently watched paths and their recursive modes.
495    ///
496    /// Returned paths use the same representation that was passed to [`Watcher::watch`] or
497    /// [`Watcher::update_paths`].
498    ///
499    /// # Errors
500    ///
501    /// Returns an error if the watcher implementation cannot provide this information.
502    fn watched_paths(&self) -> Result<Vec<(PathBuf, RecursiveMode)>> {
503        Err(Error::generic(
504            "listing watched paths is not supported by this watcher",
505        ))
506    }
507
508    /// Returns the watcher kind, allowing to perform backend-specific tasks
509    fn kind() -> WatcherKind
510    where
511        Self: Sized;
512}
513
514/// The recommended [`Watcher`] implementation for the current platform
515#[cfg(any(
516    target_os = "linux",
517    target_os = "android",
518    all(target_os = "freebsd", notify_freebsd_inotify),
519))]
520pub type RecommendedWatcher = INotifyWatcher;
521/// The recommended [`Watcher`] implementation for the current platform
522#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
523pub type RecommendedWatcher = FsEventWatcher;
524/// The recommended [`Watcher`] implementation for the current platform
525#[cfg(target_os = "windows")]
526pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
527/// The recommended [`Watcher`] implementation for the current platform
528#[cfg(any(
529    all(target_os = "freebsd", not(notify_freebsd_inotify)),
530    target_os = "openbsd",
531    target_os = "netbsd",
532    target_os = "dragonfly",
533    target_os = "ios",
534    all(target_os = "macos", feature = "macos_kqueue")
535))]
536pub type RecommendedWatcher = KqueueWatcher;
537/// The recommended [`Watcher`] implementation for the current platform
538#[cfg(not(any(
539    target_os = "linux",
540    target_os = "android",
541    target_os = "macos",
542    target_os = "windows",
543    target_os = "freebsd",
544    target_os = "openbsd",
545    target_os = "netbsd",
546    target_os = "dragonfly",
547    target_os = "ios"
548)))]
549pub type RecommendedWatcher = PollWatcher;
550
551/// Convenience method for creating the [`RecommendedWatcher`] for the current platform.
552///
553/// This is often the most ergonomic way to construct a watcher, because calling `Watcher::new(...)`
554/// requires specifying the concrete watcher type.
555///
556/// ```no_run
557/// use notify::{RecursiveMode, Result, Watcher};
558/// use std::path::Path;
559///
560/// fn main() -> Result<()> {
561///     let mut watcher = notify::recommended_watcher(|res| println!("event: {res:?}"))?;
562///     watcher.watch(Path::new("."), RecursiveMode::Recursive)?;
563///     Ok(())
564/// }
565/// ```
566pub fn recommended_watcher<F>(event_handler: F) -> Result<RecommendedWatcher>
567where
568    F: EventHandler,
569{
570    // All recommended watchers currently implement `new`, so just call that.
571    RecommendedWatcher::new(event_handler, Config::default())
572}
573
574pub(crate) fn update_paths<F>(ops: Vec<PathOp>, mut apply: F) -> StdResult<(), UpdatePathsError>
575where
576    F: FnMut(PathOp) -> StdResult<(), (PathOp, Error)>,
577{
578    let mut iter = ops.into_iter();
579    while let Some(op) = iter.next() {
580        if let Err((error_op, source)) = apply(op) {
581            return Err(UpdatePathsError {
582                source,
583                origin: Some(error_op),
584                remaining: iter.collect(),
585            });
586        }
587    }
588    Ok(())
589}
590
591#[cfg(test)]
592mod tests {
593    use std::{
594        collections::HashSet,
595        fs, iter,
596        path::{Path, PathBuf},
597        sync::mpsc,
598        time::{Duration, Instant},
599    };
600
601    use tempfile::tempdir;
602
603    use super::{
604        Config, Error, ErrorKind, Event, NullWatcher, PathOp, PollWatcher, RecommendedWatcher,
605        RecursiveMode, Result, StdResult, WatchPathConfig, Watcher, WatcherKind,
606    };
607    use crate::test::*;
608
609    #[test]
610    fn test_object_safe() {
611        let _watcher: &dyn Watcher = &NullWatcher;
612    }
613
614    #[test]
615    fn test_debug_impl() {
616        macro_rules! assert_debug_impl {
617            ($t:ty) => {{
618                #[allow(dead_code)]
619                trait NeedsDebug: std::fmt::Debug {}
620                impl NeedsDebug for $t {}
621            }};
622        }
623
624        assert_debug_impl!(Config);
625        assert_debug_impl!(Error);
626        assert_debug_impl!(ErrorKind);
627        assert_debug_impl!(NullWatcher);
628        assert_debug_impl!(PollWatcher);
629        assert_debug_impl!(RecommendedWatcher);
630        assert_debug_impl!(RecursiveMode);
631        assert_debug_impl!(WatchPathConfig);
632        assert_debug_impl!(WatcherKind);
633    }
634
635    include!("freebsd_version.rs");
636
637    fn freebsd_inotify_supported(output: &str) -> bool {
638        parse_freebsd_release(output).is_some_and(|release| release >= FREEBSD_INOTIFY_MIN)
639    }
640
641    #[test]
642    fn freebsd_inotify_release_threshold() {
643        for (version, expected) in [
644            ("13.5-RELEASE", false),
645            ("14.4-RELEASE", false),
646            ("14.4-RELEASE-p3", false),
647            ("14.4-STABLE", false),
648            ("14.5-BETA1", true),
649            ("14.5-RELEASE", true),
650            ("14.5-RELEASE-p1", true),
651            ("14.5-STABLE", true),
652            ("14.6-RELEASE", true),
653            ("15.0-RELEASE", true),
654            ("15.1-RELEASE", true),
655            ("16.0-CURRENT", true),
656        ] {
657            assert_eq!(freebsd_inotify_supported(version), expected, "{version}");
658        }
659        assert_eq!(parse_freebsd_release(""), None);
660        assert_eq!(parse_freebsd_release("14"), None);
661        assert_eq!(parse_freebsd_release("14."), None);
662    }
663
664    #[cfg(target_os = "freebsd")]
665    #[test]
666    fn recommended_watcher_matches_freebsd_version() {
667        let output = std::process::Command::new("/bin/freebsd-version")
668            .arg("-u")
669            .output()
670            .expect("run freebsd-version");
671        assert!(output.status.success(), "freebsd-version failed");
672
673        let version = std::str::from_utf8(&output.stdout).expect("parse freebsd-version output");
674        let expected = if cfg!(feature = "freebsd_inotify") || freebsd_inotify_supported(version) {
675            WatcherKind::Inotify
676        } else {
677            WatcherKind::Kqueue
678        };
679
680        assert_eq!(RecommendedWatcher::kind(), expected);
681    }
682
683    fn iter_with_timeout(rx: &mpsc::Receiver<Result<Event>>) -> impl Iterator<Item = Event> + '_ {
684        // wait for up to 10 seconds for the events
685        let deadline = Instant::now() + Duration::from_secs(10);
686        iter::from_fn(move || {
687            if Instant::now() >= deadline {
688                return None;
689            }
690            Some(
691                rx.recv_timeout(deadline - Instant::now())
692                    .expect("did not receive expected event")
693                    .expect("received an error"),
694            )
695        })
696    }
697
698    fn canonical_or_path(path: &Path) -> PathBuf {
699        path.canonicalize()
700            .expect("test paths should always be canonicalizable")
701    }
702
703    fn canonical_watch_set(
704        paths: Vec<(PathBuf, RecursiveMode)>,
705    ) -> HashSet<(PathBuf, RecursiveMode)> {
706        paths
707            .into_iter()
708            .map(|(path, recursive_mode)| (canonical_or_path(&path), recursive_mode))
709            .collect()
710    }
711
712    fn matches_path(path: &Path, expected: &Path, canonical_expected: Option<&PathBuf>) -> bool {
713        path == expected || canonical_expected.is_some_and(|canonical| path == canonical)
714    }
715
716    fn watch_with_retry(
717        watcher: &mut RecommendedWatcher,
718        path: impl AsRef<Path>,
719        recursive_mode: RecursiveMode,
720    ) -> Result<()> {
721        const FSEVENT_WATCH_RETRIES: usize = 5;
722        const FSEVENT_WATCH_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
723
724        let path = path.as_ref();
725        for attempt in 0..=FSEVENT_WATCH_RETRIES {
726            match watcher.watch(path, recursive_mode) {
727                Ok(()) => return Ok(()),
728                Err(err)
729                    if RecommendedWatcher::kind() == WatcherKind::Fsevent
730                        && matches!(
731                            &err.kind,
732                            ErrorKind::Generic(message)
733                                if message == "unable to start FSEvent stream"
734                        )
735                        && attempt < FSEVENT_WATCH_RETRIES =>
736                {
737                    let _ = watcher.unwatch(path);
738                    let delay_factor = 1u32 << attempt;
739                    std::thread::sleep(FSEVENT_WATCH_RETRY_BASE_DELAY * delay_factor);
740                }
741                Err(err) => return Err(err),
742            }
743        }
744
745        unreachable!("watch() retries must return or error")
746    }
747
748    fn update_paths_unwatch_with_retry(
749        watcher: &mut RecommendedWatcher,
750        path: &Path,
751    ) -> Result<()> {
752        const FSEVENT_UNWATCH_RETRIES: usize = 5;
753        const FSEVENT_UNWATCH_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
754
755        for attempt in 0..=FSEVENT_UNWATCH_RETRIES {
756            match watcher.update_paths(vec![PathOp::unwatch(path)]) {
757                Ok(()) => return Ok(()),
758                Err(err)
759                    if RecommendedWatcher::kind() == WatcherKind::Fsevent
760                        && matches!(
761                            &err.source.kind,
762                            ErrorKind::Io(io_err) if io_err.raw_os_error() == Some(9)
763                        )
764                        && attempt < FSEVENT_UNWATCH_RETRIES =>
765                {
766                    let delay_factor = 1u32 << attempt;
767                    std::thread::sleep(FSEVENT_UNWATCH_RETRY_BASE_DELAY * delay_factor);
768                }
769                Err(err) => return Err(err.into()),
770            }
771        }
772
773        unreachable!("fsevent unwatch retries must return or error")
774    }
775
776    #[test]
777    fn integration() -> std::result::Result<(), Box<dyn std::error::Error>> {
778        let dir = tempdir()?;
779
780        // set up the watcher
781        let (tx, rx) = std::sync::mpsc::channel();
782        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
783        watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
784
785        // create a new file
786        let file_path = dir.path().join("file.txt");
787        fs::write(&file_path, b"Lorem ipsum")?;
788
789        println!("waiting for event at {}", file_path.display());
790
791        // wait for the create event, ignore all other events
792        for event in iter_with_timeout(&rx) {
793            if event.paths == vec![file_path.clone()]
794                || event.paths == vec![file_path.canonicalize()?]
795            {
796                return Ok(());
797            }
798
799            println!("unexpected event: {event:?}");
800        }
801
802        panic!("did not receive expected event");
803    }
804
805    #[test]
806    fn event_paths_preserve_relative_watch_root()
807    -> std::result::Result<(), Box<dyn std::error::Error>> {
808        let cwd = std::env::current_dir()?;
809        let dir = tempfile::Builder::new()
810            .prefix("notify-relative-")
811            .tempdir_in(&cwd)?;
812        let relative_dir = dir.path().strip_prefix(&cwd)?.to_path_buf();
813        let relative_file = relative_dir.join("file.txt");
814        let absolute_file = dir.path().join("file.txt");
815
816        let (tx, rx) = std::sync::mpsc::channel();
817        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
818        watch_with_retry(&mut watcher, &relative_dir, RecursiveMode::Recursive)?;
819
820        assert!(
821            watcher
822                .watched_paths()?
823                .iter()
824                .any(|(path, mode)| path == &relative_dir && *mode == RecursiveMode::Recursive),
825            "watched_paths() did not preserve relative watch path"
826        );
827
828        fs::write(&absolute_file, b"Lorem ipsum")?;
829
830        for event in iter_with_timeout(&rx) {
831            if event.paths.iter().any(|path| path == &relative_file) {
832                assert!(
833                    event.paths.iter().all(|path| path.is_relative()),
834                    "relative watch emitted absolute path: {event:?}"
835                );
836                return Ok(());
837            }
838
839            println!("unexpected event: {event:?}");
840        }
841
842        panic!("did not receive expected relative event path");
843    }
844
845    #[test]
846    #[cfg(target_os = "windows")]
847    fn test_windows_trash_dir() -> std::result::Result<(), Box<dyn std::error::Error>> {
848        use crate::recommended_watcher;
849
850        let dir = tempdir()?;
851        let child_dir = dir.path().join("child");
852        fs::create_dir(&child_dir)?;
853
854        let mut watcher = recommended_watcher(|_| {
855            // Do something with the event
856        })?;
857        watcher.watch(&child_dir, RecursiveMode::NonRecursive)?;
858
859        trash::delete(&child_dir)?;
860
861        watcher.watch(dir.path(), RecursiveMode::NonRecursive)?;
862
863        Ok(())
864    }
865
866    #[test]
867    fn test_update_paths() -> std::result::Result<(), Box<dyn std::error::Error>> {
868        let dir = tempdir()?;
869
870        let dir_a = dir.path().join("a");
871        let dir_b = dir.path().join("b");
872
873        fs::create_dir(&dir_a)?;
874        fs::create_dir(&dir_b)?;
875
876        let (tx, rx) = std::sync::mpsc::channel();
877        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
878
879        // start watching a and b
880        const FSEVENT_UPDATE_PATHS_RETRIES: usize = 5;
881        const FSEVENT_UPDATE_PATHS_RETRY_BASE_DELAY: Duration = Duration::from_millis(50);
882        for attempt in 0..=FSEVENT_UPDATE_PATHS_RETRIES {
883            match watcher.update_paths(vec![
884                PathOp::Watch(
885                    dir_a.clone(),
886                    WatchPathConfig::new(RecursiveMode::Recursive),
887                ),
888                PathOp::Watch(
889                    dir_b.clone(),
890                    WatchPathConfig::new(RecursiveMode::Recursive),
891                ),
892            ]) {
893                Ok(()) => break,
894                Err(err)
895                    if RecommendedWatcher::kind() == WatcherKind::Fsevent
896                        && matches!(
897                            &err.source.kind,
898                            ErrorKind::Generic(message)
899                            if message == "unable to start FSEvent stream"
900                        )
901                        && attempt < FSEVENT_UPDATE_PATHS_RETRIES =>
902                {
903                    let delay_factor = 1u32 << attempt;
904                    std::thread::sleep(FSEVENT_UPDATE_PATHS_RETRY_BASE_DELAY * delay_factor);
905                }
906                Err(err) => return Err(err.into()),
907            }
908        }
909
910        // create file1 in both a and b
911        let a_file1 = dir_a.join("file1");
912        let b_file1 = dir_b.join("file1");
913        fs::write(&a_file1, b"Lorem ipsum")?;
914        fs::write(&b_file1, b"Lorem ipsum")?;
915        let a_file1_canonical = a_file1.canonicalize().ok();
916        let b_file1_canonical = b_file1.canonicalize().ok();
917
918        // wait for create events of a/file1 and b/file1
919        let mut a_file1_encountered: bool = false;
920        let mut b_file1_encountered: bool = false;
921        for event in iter_with_timeout(&rx) {
922            for path in event.paths {
923                a_file1_encountered = a_file1_encountered
924                    || matches_path(&path, &a_file1, a_file1_canonical.as_ref());
925                b_file1_encountered = b_file1_encountered
926                    || matches_path(&path, &b_file1, b_file1_canonical.as_ref());
927            }
928            if a_file1_encountered && b_file1_encountered {
929                break;
930            }
931        }
932        assert!(a_file1_encountered, "Did not receive event of {a_file1:?}");
933        assert!(b_file1_encountered, "Did not receive event of {b_file1:?}");
934
935        // stop watching a
936        update_paths_unwatch_with_retry(&mut watcher, &dir_a)?;
937
938        // create file2 in both a and b
939        let a_file2 = dir_a.join("file2");
940        let b_file2 = dir_b.join("file2");
941        fs::write(&a_file2, b"Lorem ipsum")?;
942        fs::write(&b_file2, b"Lorem ipsum")?;
943        let a_file2_canonical = a_file2.canonicalize().ok();
944        let b_file2_canonical = b_file2.canonicalize().ok();
945
946        // wait for the create event of b/file2 only
947        for event in iter_with_timeout(&rx) {
948            for path in event.paths {
949                assert!(
950                    !matches_path(&path, &a_file2, a_file2_canonical.as_ref()),
951                    "Event of {a_file2:?} should not be received"
952                );
953                if matches_path(&path, &b_file2, b_file2_canonical.as_ref()) {
954                    return Ok(());
955                }
956            }
957        }
958        panic!("Did not receive the event of {b_file2:?}");
959    }
960
961    #[test]
962    fn watched_paths_reflect_watch_and_unwatch()
963    -> std::result::Result<(), Box<dyn std::error::Error>> {
964        let dir = tempdir()?;
965        let dir_a = dir.path().join("a");
966        let dir_b = dir.path().join("b");
967        fs::create_dir(&dir_a)?;
968        fs::create_dir(&dir_b)?;
969
970        let (tx, _rx) = std::sync::mpsc::channel();
971        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
972
973        watch_with_retry(&mut watcher, &dir_a, RecursiveMode::Recursive)?;
974        watch_with_retry(&mut watcher, &dir_b, RecursiveMode::NonRecursive)?;
975
976        let watched = canonical_watch_set(watcher.watched_paths()?);
977        assert!(watched.contains(&(canonical_or_path(&dir_a), RecursiveMode::Recursive)));
978        assert!(watched.contains(&(canonical_or_path(&dir_b), RecursiveMode::NonRecursive)));
979
980        watcher.unwatch(&dir_a)?;
981
982        let watched = canonical_watch_set(watcher.watched_paths()?);
983        assert!(!watched.contains(&(canonical_or_path(&dir_a), RecursiveMode::Recursive)));
984        assert!(watched.contains(&(canonical_or_path(&dir_b), RecursiveMode::NonRecursive)));
985
986        Ok(())
987    }
988
989    #[test]
990    fn rewatching_same_path_replaces_recursive_mode()
991    -> std::result::Result<(), Box<dyn std::error::Error>> {
992        let dir = tempdir()?;
993        let root = canonical_or_path(dir.path());
994
995        let (tx, _rx) = std::sync::mpsc::channel();
996        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
997
998        watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
999        watch_with_retry(&mut watcher, dir.path(), RecursiveMode::NonRecursive)?;
1000
1001        let watched = canonical_watch_set(watcher.watched_paths()?);
1002        assert!(watched.contains(&(root.clone(), RecursiveMode::NonRecursive)));
1003        assert!(!watched.contains(&(root.clone(), RecursiveMode::Recursive)));
1004        assert_eq!(
1005            watched.iter().filter(|(path, _mode)| path == &root).count(),
1006            1
1007        );
1008
1009        watcher.unwatch(dir.path())?;
1010
1011        let watched = canonical_watch_set(watcher.watched_paths()?);
1012        assert!(!watched.iter().any(|(path, _mode)| path == &root));
1013
1014        Ok(())
1015    }
1016
1017    #[test]
1018    fn overlapping_recursive_watch_preserves_explicit_child()
1019    -> std::result::Result<(), Box<dyn std::error::Error>> {
1020        let dir = tempdir()?;
1021        let child = dir.path().join("child");
1022        fs::create_dir(&child)?;
1023
1024        let (tx, _rx) = std::sync::mpsc::channel();
1025        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1026
1027        watch_with_retry(&mut watcher, &child, RecursiveMode::NonRecursive)?;
1028        watch_with_retry(&mut watcher, dir.path(), RecursiveMode::Recursive)?;
1029
1030        let watched = canonical_watch_set(watcher.watched_paths()?);
1031        assert!(watched.contains(&(canonical_or_path(dir.path()), RecursiveMode::Recursive)));
1032        assert!(watched.contains(&(canonical_or_path(&child), RecursiveMode::NonRecursive)));
1033
1034        watcher.unwatch(dir.path())?;
1035
1036        let watched = canonical_watch_set(watcher.watched_paths()?);
1037        assert!(!watched.contains(&(canonical_or_path(dir.path()), RecursiveMode::Recursive)));
1038        assert!(watched.contains(&(canonical_or_path(&child), RecursiveMode::NonRecursive)));
1039
1040        Ok(())
1041    }
1042
1043    #[test]
1044    fn overlapping_recursive_child_rewrites_descendant_event_paths()
1045    -> std::result::Result<(), Box<dyn std::error::Error>> {
1046        let cwd = std::env::current_dir()?;
1047        let dir = tempfile::Builder::new()
1048            .prefix("notify-overlap-")
1049            .tempdir_in(&cwd)?;
1050        let relative_dir = dir.path().strip_prefix(&cwd)?.to_path_buf();
1051        let child = dir.path().join("child");
1052        let grandchild = child.join("grandchild");
1053        fs::create_dir_all(&grandchild)?;
1054
1055        let (tx, rx) = std::sync::mpsc::channel();
1056        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1057
1058        watch_with_retry(&mut watcher, &relative_dir, RecursiveMode::Recursive)?;
1059        watch_with_retry(&mut watcher, &child, RecursiveMode::Recursive)?;
1060        watcher.unwatch(&relative_dir)?;
1061
1062        let watched = watcher.watched_paths()?;
1063        assert!(
1064            watched
1065                .iter()
1066                .any(|(path, mode)| path == &child && *mode == RecursiveMode::Recursive),
1067            "watched_paths() did not preserve explicit child path: {watched:?}"
1068        );
1069
1070        let file = grandchild.join("file.txt");
1071        let stale_file = relative_dir
1072            .join("child")
1073            .join("grandchild")
1074            .join("file.txt");
1075        fs::write(&file, b"Lorem ipsum")?;
1076
1077        for event in iter_with_timeout(&rx) {
1078            if event.paths.iter().any(|path| path == &file) {
1079                assert!(
1080                    event.paths.iter().all(|path| path.is_absolute()),
1081                    "absolute child watch emitted non-absolute path: {event:?}"
1082                );
1083                return Ok(());
1084            }
1085
1086            assert!(
1087                !event.paths.iter().any(|path| path == &stale_file),
1088                "stale parent-relative file path after parent unwatch: {event:?}"
1089            );
1090        }
1091
1092        panic!("did not receive expected child event path");
1093    }
1094
1095    #[test]
1096    fn update_paths_in_a_loop_with_errors() -> StdResult<(), Box<dyn std::error::Error>> {
1097        let dir = tempdir()?;
1098        let existing_dir_1 = dir.path().join("existing_dir_1");
1099        let not_existent_file = dir.path().join("not_existent_file");
1100        let existing_dir_2 = dir.path().join("existing_dir_2");
1101
1102        fs::create_dir(&existing_dir_1)?;
1103        fs::create_dir(&existing_dir_2)?;
1104
1105        let mut paths_to_add = vec![
1106            PathOp::watch_recursive(existing_dir_1.clone()),
1107            PathOp::watch_recursive(not_existent_file.clone()),
1108            PathOp::watch_recursive(existing_dir_2.clone()),
1109        ];
1110
1111        let (tx, rx) = std::sync::mpsc::channel();
1112        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
1113
1114        while !paths_to_add.is_empty() {
1115            if let Err(e) = watcher.update_paths(std::mem::take(&mut paths_to_add)) {
1116                paths_to_add = e.remaining;
1117            }
1118        }
1119
1120        fs::write(existing_dir_1.join("1"), "")?;
1121        fs::write(&not_existent_file, "")?;
1122        let waiting_path = existing_dir_2.join("1");
1123        fs::write(&waiting_path, "")?;
1124        let waiting_path_canonical = waiting_path.canonicalize().ok();
1125
1126        for event in iter_with_timeout(&rx) {
1127            let path = event
1128                .paths
1129                .first()
1130                .unwrap_or_else(|| panic!("event must have a path: {event:?}"));
1131            assert!(
1132                path != &not_existent_file,
1133                "unexpected {not_existent_file:?} event"
1134            );
1135            if matches_path(path, &waiting_path, waiting_path_canonical.as_ref()) {
1136                return Ok(());
1137            }
1138        }
1139
1140        panic!("Did not receive the event of {waiting_path:?}");
1141    }
1142
1143    #[test]
1144    fn create_file() {
1145        let tmpdir = testdir();
1146        let (mut watcher, mut rx) = recommended_channel();
1147        watcher.watch_recursively(&tmpdir);
1148
1149        let path = tmpdir.path().join("entry");
1150        std::fs::File::create_new(&path).expect("create");
1151
1152        rx.wait_unordered([expected(path).create()]);
1153    }
1154
1155    #[test]
1156    fn create_dir() {
1157        let tmpdir = testdir();
1158        let (mut watcher, mut rx) = recommended_channel();
1159        watcher.watch_recursively(&tmpdir);
1160
1161        let path = tmpdir.path().join("entry");
1162        std::fs::create_dir(&path).expect("create");
1163
1164        rx.wait_unordered([expected(path).create()]);
1165    }
1166
1167    #[test]
1168    fn modify_file() {
1169        let tmpdir = testdir();
1170        let (mut watcher, mut rx) = recommended_channel();
1171
1172        let path = tmpdir.path().join("entry");
1173        std::fs::File::create_new(&path).expect("create");
1174
1175        watcher.watch_recursively(&tmpdir);
1176        std::fs::write(&path, b"123").expect("write");
1177
1178        rx.wait_unordered([expected(path).modify()]);
1179    }
1180
1181    #[test]
1182    fn remove_file() {
1183        let tmpdir = testdir();
1184        let (mut watcher, mut rx) = recommended_channel();
1185
1186        let path = tmpdir.path().join("entry");
1187        std::fs::File::create_new(&path).expect("create");
1188
1189        watcher.watch_recursively(&tmpdir);
1190        std::fs::remove_file(&path).expect("remove");
1191
1192        rx.wait_unordered([expected(path).remove()]);
1193    }
1194
1195    #[cfg(feature = "futures")]
1196    #[tokio::test]
1197    async fn futures_unbounded_sender_as_handler() {
1198        use crate::recommended_watcher;
1199        use futures::StreamExt;
1200
1201        let tmpdir = testdir();
1202        let (tx, mut rx) = futures::channel::mpsc::unbounded();
1203        let mut watcher = recommended_watcher(tx).unwrap();
1204        watcher
1205            .watch(tmpdir.path(), RecursiveMode::NonRecursive)
1206            .unwrap();
1207
1208        std::fs::create_dir(tmpdir.path().join("1")).unwrap();
1209
1210        tokio::time::timeout(Duration::from_secs(5), rx.next())
1211            .await
1212            .expect("timeout")
1213            .expect("No event")
1214            .expect("Error");
1215    }
1216
1217    #[cfg(feature = "tokio")]
1218    #[tokio::test]
1219    async fn tokio_unbounded_sender_as_handler() {
1220        use crate::recommended_watcher;
1221
1222        let tmpdir = testdir();
1223        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1224        let mut watcher = recommended_watcher(tx).unwrap();
1225        watcher
1226            .watch(tmpdir.path(), RecursiveMode::NonRecursive)
1227            .unwrap();
1228
1229        std::fs::create_dir(tmpdir.path().join("1")).unwrap();
1230
1231        tokio::time::timeout(Duration::from_secs(5), rx.recv())
1232            .await
1233            .expect("timeout")
1234            .expect("No event")
1235            .expect("Error");
1236    }
1237
1238    #[test]
1239    fn update_paths_error_contains_errored_path() {
1240        let err = super::update_paths(
1241            [
1242                PathOp::unwatch("1"),
1243                PathOp::unwatch("2"),
1244                PathOp::unwatch("3"),
1245            ]
1246            .into(),
1247            |op| {
1248                if op.as_path() == Path::new("2") {
1249                    Err((op, super::Error::path_not_found()))
1250                } else {
1251                    Ok(())
1252                }
1253            },
1254        )
1255        .unwrap_err();
1256        assert_eq!(
1257            &err.into_iter().map(PathOp::into_path).collect::<Vec<_>>(),
1258            &[PathBuf::from("2"), PathBuf::from("3"),]
1259        )
1260    }
1261}