notify/lib.rs
1//! Cross-platform file system notification library
2//!
3//! # Installation
4//!
5//! ```toml
6//! [dependencies]
7//! notify = "8.1.0"
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` enabled by default, for fsevent backend on macos
19//! - `macos_kqueue` for kqueue backend on macos
20//! - `serialization-compat-6` restores the serialization behavior of notify 6, off by default
21//!
22//! ### Serde
23//!
24//! Events are serializable via [serde](https://serde.rs) if the `serde` feature is enabled:
25//!
26//! ```toml
27//! notify = { version = "8.1.0", features = ["serde"] }
28//! ```
29//!
30//! # Known Problems
31//!
32//! ### Network filesystems
33//!
34//! Network mounted filesystems like NFS may not emit any events for notify to listen to.
35//! This applies especially to WSL programs watching windows paths ([issue #254](https://github.com/notify-rs/notify/issues/254)).
36//!
37//! A workaround is the [`PollWatcher`] backend.
38//!
39//! ### Docker with Linux on macOS M1
40//!
41//! Docker on macOS M1 [throws](https://github.com/notify-rs/notify/issues/423) `Function not implemented (os error 38)`.
42//! You have to manually use the [`PollWatcher`], as the native backend isn't available inside the emulation.
43//!
44//! ### macOS, FSEvents and unowned files
45//!
46//! Due to the inner security model of FSEvents (see [FileSystemEventSecurity](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/FileSystemEventSecurity/FileSystemEventSecurity.html)),
47//! some events cannot be observed easily when trying to follow files that do not
48//! belong to you. In this case, reverting to the pollwatcher can fix the issue,
49//! with a slight performance cost.
50//!
51//! ### Editor Behaviour
52//!
53//! If you rely on precise events (Write/Delete/Create..), you will notice that the actual events
54//! can differ a lot between file editors. Some truncate the file on save, some create a new one and replace the old one.
55//! 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.
56//!
57//! ### Parent folder deletion
58//!
59//! 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`.
60//! See [here](https://github.com/notify-rs/notify/issues/403) for more details.
61//!
62//! ### Pseudo Filesystems like /proc, /sys
63//!
64//! Some filesystems like `/proc` and `/sys` on *nix do not emit change events or use correct file change dates.
65//! To circumvent that problem you can use the [`PollWatcher`] with the `compare_contents` option.
66//!
67//! ### Linux: Bad File Descriptor / No space left on device
68//!
69//! This may be the case of running into the max-files watched limits of your user or system.
70//! (Files also includes folders.) Note that for recursive watched folders each file and folder inside counts towards the limit.
71//!
72//! You may increase this limit in linux via
73//! ```sh
74//! sudo sysctl fs.inotify.max_user_instances=8192 # example number
75//! sudo sysctl fs.inotify.max_user_watches=524288 # example number
76//! sudo sysctl -p
77//! ```
78//!
79//! Note that the [`PollWatcher`] is not restricted by this limitation, so it may be an alternative if your users can't increase the limit.
80//!
81//! ### Watching large directories
82//!
83//! When watching a very large amount of files, notify may fail to receive all events.
84//! 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).
85//!
86//! # Examples
87//!
88//! For more examples visit the [examples folder](https://github.com/notify-rs/notify/tree/main/examples) in the repository.
89//!
90//! ```rust
91//! use notify::{Event, RecursiveMode, Result, Watcher};
92//! use std::{path::Path, sync::mpsc};
93//!
94//! fn main() -> Result<()> {
95//! let (tx, rx) = mpsc::channel::<Result<Event>>();
96//!
97//! // Use recommended_watcher() to automatically select the best implementation
98//! // for your platform. The `EventHandler` passed to this constructor can be a
99//! // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or
100//! // another type the trait is implemented for.
101//! let mut watcher = notify::recommended_watcher(tx)?;
102//!
103//! // Add a path to be watched. All files and directories at that path and
104//! // below will be monitored for changes.
105//! # #[cfg(not(any(
106//! # target_os = "freebsd",
107//! # target_os = "openbsd",
108//! # target_os = "dragonfly",
109//! # target_os = "netbsd")))]
110//! # { // "." doesn't exist on BSD for some reason in CI
111//! watcher.watch(Path::new("."), RecursiveMode::Recursive)?;
112//! # }
113//! # #[cfg(any())]
114//! # { // don't run this in doctests, it blocks forever
115//! // Block forever, printing out events as they come in
116//! for res in rx {
117//! match res {
118//! Ok(event) => println!("event: {:?}", event),
119//! Err(e) => println!("watch error: {:?}", e),
120//! }
121//! }
122//! # }
123//!
124//! Ok(())
125//! }
126//! ```
127//!
128//! ## With different configurations
129//!
130//! It is possible to create several watchers with different configurations or implementations that
131//! all call the same event function. This can accommodate advanced behaviour or work around limits.
132//!
133//! ```rust
134//! # use notify::{RecursiveMode, Result, Watcher};
135//! # use std::path::Path;
136//! #
137//! # fn main() -> Result<()> {
138//! fn event_fn(res: Result<notify::Event>) {
139//! match res {
140//! Ok(event) => println!("event: {:?}", event),
141//! Err(e) => println!("watch error: {:?}", e),
142//! }
143//! }
144//!
145//! let mut watcher1 = notify::recommended_watcher(event_fn)?;
146//! // we will just use the same watcher kind again here
147//! let mut watcher2 = notify::recommended_watcher(event_fn)?;
148//! # #[cfg(not(any(
149//! # target_os = "freebsd",
150//! # target_os = "openbsd",
151//! # target_os = "dragonfly",
152//! # target_os = "netbsd")))]
153//! # { // "." doesn't exist on BSD for some reason in CI
154//! # watcher1.watch(Path::new("."), RecursiveMode::Recursive)?;
155//! # watcher2.watch(Path::new("."), RecursiveMode::Recursive)?;
156//! # }
157//! // dropping the watcher1/2 here (no loop etc) will end the program
158//! #
159//! # Ok(())
160//! # }
161//! ```
162
163#![deny(missing_docs)]
164
165pub use config::{Config, RecursiveMode};
166pub use error::{Error, ErrorKind, Result};
167pub use notify_types::event::{self, Event, EventKind, EventKindMask};
168use std::path::Path;
169
170pub(crate) type Receiver<T> = std::sync::mpsc::Receiver<T>;
171pub(crate) type Sender<T> = std::sync::mpsc::Sender<T>;
172#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
173pub(crate) type BoundSender<T> = std::sync::mpsc::SyncSender<T>;
174
175#[inline]
176pub(crate) fn unbounded<T>() -> (Sender<T>, Receiver<T>) {
177 std::sync::mpsc::channel()
178}
179
180#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
181#[inline]
182pub(crate) fn bounded<T>(cap: usize) -> (BoundSender<T>, Receiver<T>) {
183 std::sync::mpsc::sync_channel(cap)
184}
185
186#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
187pub use crate::fsevent::FsEventWatcher;
188#[cfg(any(target_os = "linux", target_os = "android"))]
189pub use crate::inotify::INotifyWatcher;
190#[cfg(any(
191 target_os = "freebsd",
192 target_os = "openbsd",
193 target_os = "netbsd",
194 target_os = "dragonfly",
195 target_os = "ios",
196 all(target_os = "macos", feature = "macos_kqueue")
197))]
198pub use crate::kqueue::KqueueWatcher;
199pub use null::NullWatcher;
200pub use poll::PollWatcher;
201#[cfg(target_os = "windows")]
202pub use windows::ReadDirectoryChangesWatcher;
203
204#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
205pub mod fsevent;
206#[cfg(any(target_os = "linux", target_os = "android"))]
207pub mod inotify;
208#[cfg(any(
209 target_os = "freebsd",
210 target_os = "openbsd",
211 target_os = "dragonfly",
212 target_os = "netbsd",
213 target_os = "ios",
214 all(target_os = "macos", feature = "macos_kqueue")
215))]
216pub mod kqueue;
217#[cfg(target_os = "windows")]
218pub mod windows;
219
220pub mod null;
221pub mod poll;
222
223mod config;
224mod error;
225
226#[cfg(test)]
227pub(crate) mod test;
228
229/// The set of requirements for watcher event handling functions.
230///
231/// # Example implementation
232///
233/// ```no_run
234/// use notify::{Event, Result, EventHandler};
235///
236/// /// Prints received events
237/// struct EventPrinter;
238///
239/// impl EventHandler for EventPrinter {
240/// fn handle_event(&mut self, event: Result<Event>) {
241/// if let Ok(event) = event {
242/// println!("Event: {:?}", event);
243/// }
244/// }
245/// }
246/// ```
247pub trait EventHandler: Send + 'static {
248 /// Handles an event.
249 fn handle_event(&mut self, event: Result<Event>);
250}
251
252impl<F> EventHandler for F
253where
254 F: FnMut(Result<Event>) + Send + 'static,
255{
256 fn handle_event(&mut self, event: Result<Event>) {
257 (self)(event);
258 }
259}
260
261#[cfg(feature = "crossbeam-channel")]
262impl EventHandler for crossbeam_channel::Sender<Result<Event>> {
263 fn handle_event(&mut self, event: Result<Event>) {
264 let _ = self.send(event);
265 }
266}
267
268#[cfg(feature = "flume")]
269impl EventHandler for flume::Sender<Result<Event>> {
270 fn handle_event(&mut self, event: Result<Event>) {
271 let _ = self.send(event);
272 }
273}
274
275impl EventHandler for std::sync::mpsc::Sender<Result<Event>> {
276 fn handle_event(&mut self, event: Result<Event>) {
277 let _ = self.send(event);
278 }
279}
280
281/// Watcher kind enumeration
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
283#[non_exhaustive]
284pub enum WatcherKind {
285 /// inotify backend (linux)
286 Inotify,
287 /// FS-Event backend (mac)
288 Fsevent,
289 /// KQueue backend (bsd,optionally mac)
290 Kqueue,
291 /// Polling based backend (fallback)
292 PollWatcher,
293 /// Windows backend
294 ReadDirectoryChangesWatcher,
295 /// Fake watcher for testing
296 NullWatcher,
297}
298
299/// Providing methods for adding and removing paths to watch.
300///
301/// `Box<dyn PathsMut>` is created by [`Watcher::paths_mut`]. See its documentation for more.
302pub trait PathsMut {
303 /// Add a new path to watch. See [`Watcher::watch`] for more.
304 fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
305
306 /// Remove a path from watching. See [`Watcher::unwatch`] for more.
307 fn remove(&mut self, path: &Path) -> Result<()>;
308
309 /// Ensure added/removed paths are applied.
310 ///
311 /// The behaviour of dropping a [`PathsMut`] without calling [`commit`] is unspecified.
312 /// The implementation is free to ignore the changes or not, and may leave the watcher in a started or stopped state.
313 fn commit(self: Box<Self>) -> Result<()>;
314}
315
316/// Type that can deliver file activity notifications
317///
318/// `Watcher` is implemented per platform using the best implementation available on that platform.
319/// In addition to such event driven implementations, a polling implementation is also provided
320/// that should work on any platform.
321pub trait Watcher {
322 /// Create a new watcher with an initial Config.
323 fn new<F: EventHandler>(event_handler: F, config: config::Config) -> Result<Self>
324 where
325 Self: Sized;
326 /// Begin watching a new path.
327 ///
328 /// If the `path` is a directory, `recursive_mode` will be evaluated. If `recursive_mode` is
329 /// `RecursiveMode::Recursive` events will be delivered for all files in that tree. Otherwise
330 /// only the directory and its immediate children will be watched.
331 ///
332 /// If the `path` is a file, `recursive_mode` will be ignored and events will be delivered only
333 /// for the file.
334 ///
335 /// On some platforms, if the `path` is renamed or removed while being watched, behaviour may
336 /// be unexpected. See discussions in [#165] and [#166]. If less surprising behaviour is wanted
337 /// one may non-recursively watch the _parent_ directory as well and manage related events.
338 ///
339 /// [#165]: https://github.com/notify-rs/notify/issues/165
340 /// [#166]: https://github.com/notify-rs/notify/issues/166
341 fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
342
343 /// Stop watching a path.
344 ///
345 /// # Errors
346 ///
347 /// Returns an error in the case that `path` has not been watched or if removing the watch
348 /// fails.
349 fn unwatch(&mut self, path: &Path) -> Result<()>;
350
351 /// Add/remove paths to watch.
352 ///
353 /// For some watcher implementations this method provides better performance than multiple calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once.
354 ///
355 /// # Examples
356 ///
357 /// ```
358 /// # use notify::{Watcher, RecursiveMode, Result};
359 /// # use std::path::Path;
360 /// # fn main() -> Result<()> {
361 /// # let many_paths_to_add = vec![];
362 /// let mut watcher = notify::recommended_watcher(|_event| { /* event handler */ })?;
363 /// let mut watcher_paths = watcher.paths_mut();
364 /// for path in many_paths_to_add {
365 /// watcher_paths.add(path, RecursiveMode::Recursive)?;
366 /// }
367 /// watcher_paths.commit()?;
368 /// # Ok(())
369 /// # }
370 /// ```
371 fn paths_mut<'me>(&'me mut self) -> Box<dyn PathsMut + 'me> {
372 struct DefaultPathsMut<'a, T: ?Sized>(&'a mut T);
373 impl<T: Watcher + ?Sized> PathsMut for DefaultPathsMut<'_, T> {
374 fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> {
375 self.0.watch(path, recursive_mode)
376 }
377 fn remove(&mut self, path: &Path) -> Result<()> {
378 self.0.unwatch(path)
379 }
380 fn commit(self: Box<Self>) -> Result<()> {
381 Ok(())
382 }
383 }
384 Box::new(DefaultPathsMut(self))
385 }
386
387 /// Configure the watcher at runtime.
388 ///
389 /// See the [`Config`](config/struct.Config.html) struct for all configuration options.
390 ///
391 /// # Returns
392 ///
393 /// - `Ok(true)` on success.
394 /// - `Ok(false)` if the watcher does not support or implement the option.
395 /// - `Err(notify::Error)` on failure.
396 fn configure(&mut self, _option: Config) -> Result<bool> {
397 Ok(false)
398 }
399
400 /// Returns the watcher kind, allowing to perform backend-specific tasks
401 fn kind() -> WatcherKind
402 where
403 Self: Sized;
404}
405
406/// The recommended [`Watcher`] implementation for the current platform
407#[cfg(any(target_os = "linux", target_os = "android"))]
408pub type RecommendedWatcher = INotifyWatcher;
409/// The recommended [`Watcher`] implementation for the current platform
410#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
411pub type RecommendedWatcher = FsEventWatcher;
412/// The recommended [`Watcher`] implementation for the current platform
413#[cfg(target_os = "windows")]
414pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
415/// The recommended [`Watcher`] implementation for the current platform
416#[cfg(any(
417 target_os = "freebsd",
418 target_os = "openbsd",
419 target_os = "netbsd",
420 target_os = "dragonfly",
421 target_os = "ios",
422 all(target_os = "macos", feature = "macos_kqueue")
423))]
424pub type RecommendedWatcher = KqueueWatcher;
425/// The recommended [`Watcher`] implementation for the current platform
426#[cfg(not(any(
427 target_os = "linux",
428 target_os = "android",
429 target_os = "macos",
430 target_os = "windows",
431 target_os = "freebsd",
432 target_os = "openbsd",
433 target_os = "netbsd",
434 target_os = "dragonfly",
435 target_os = "ios"
436)))]
437pub type RecommendedWatcher = PollWatcher;
438
439/// Convenience method for creating the [`RecommendedWatcher`] for the current platform.
440pub fn recommended_watcher<F>(event_handler: F) -> Result<RecommendedWatcher>
441where
442 F: EventHandler,
443{
444 // All recommended watchers currently implement `new`, so just call that.
445 RecommendedWatcher::new(event_handler, Config::default())
446}
447
448#[cfg(test)]
449mod tests {
450 use std::{
451 fs, iter,
452 sync::mpsc,
453 time::{Duration, Instant},
454 };
455
456 use tempfile::tempdir;
457
458 use super::{
459 Config, Error, ErrorKind, Event, NullWatcher, PollWatcher, RecommendedWatcher,
460 RecursiveMode, Result, Watcher, WatcherKind,
461 };
462 use crate::test::*;
463
464 #[test]
465 fn test_object_safe() {
466 let _watcher: &dyn Watcher = &NullWatcher;
467 }
468
469 #[test]
470 fn test_debug_impl() {
471 macro_rules! assert_debug_impl {
472 ($t:ty) => {{
473 #[allow(dead_code)]
474 trait NeedsDebug: std::fmt::Debug {}
475 impl NeedsDebug for $t {}
476 }};
477 }
478
479 assert_debug_impl!(Config);
480 assert_debug_impl!(Error);
481 assert_debug_impl!(ErrorKind);
482 assert_debug_impl!(NullWatcher);
483 assert_debug_impl!(PollWatcher);
484 assert_debug_impl!(RecommendedWatcher);
485 assert_debug_impl!(RecursiveMode);
486 assert_debug_impl!(WatcherKind);
487 }
488
489 fn iter_with_timeout(rx: &mpsc::Receiver<Result<Event>>) -> impl Iterator<Item = Event> + '_ {
490 // wait for up to 10 seconds for the events
491 let deadline = Instant::now() + Duration::from_secs(10);
492 iter::from_fn(move || {
493 if Instant::now() >= deadline {
494 return None;
495 }
496 Some(
497 rx.recv_timeout(deadline - Instant::now())
498 .expect("did not receive expected event")
499 .expect("received an error"),
500 )
501 })
502 }
503
504 #[test]
505 fn integration() -> std::result::Result<(), Box<dyn std::error::Error>> {
506 let dir = tempdir()?;
507
508 // set up the watcher
509 let (tx, rx) = std::sync::mpsc::channel();
510 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
511 watcher.watch(dir.path(), RecursiveMode::Recursive)?;
512
513 // create a new file
514 let file_path = dir.path().join("file.txt");
515 fs::write(&file_path, b"Lorem ipsum")?;
516
517 println!("waiting for event at {}", file_path.display());
518
519 // wait for the create event, ignore all other events
520 for event in iter_with_timeout(&rx) {
521 if event.paths == vec![file_path.clone()]
522 || event.paths == vec![file_path.canonicalize()?]
523 {
524 return Ok(());
525 }
526
527 println!("unexpected event: {event:?}");
528 }
529
530 panic!("did not receive expected event");
531 }
532
533 #[test]
534 fn test_paths_mut() -> std::result::Result<(), Box<dyn std::error::Error>> {
535 let dir = tempdir()?;
536
537 let dir_a = dir.path().join("a");
538 let dir_b = dir.path().join("b");
539
540 fs::create_dir(&dir_a)?;
541 fs::create_dir(&dir_b)?;
542
543 let (tx, rx) = std::sync::mpsc::channel();
544 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
545
546 // start watching a and b
547 {
548 let mut watcher_paths = watcher.paths_mut();
549 watcher_paths.add(&dir_a, RecursiveMode::Recursive)?;
550 watcher_paths.add(&dir_b, RecursiveMode::Recursive)?;
551 watcher_paths.commit()?;
552 }
553
554 // create file1 in both a and b
555 let a_file1 = dir_a.join("file1");
556 let b_file1 = dir_b.join("file1");
557 fs::write(&a_file1, b"Lorem ipsum")?;
558 fs::write(&b_file1, b"Lorem ipsum")?;
559
560 // wait for create events of a/file1 and b/file1
561 let mut a_file1_encountered: bool = false;
562 let mut b_file1_encountered: bool = false;
563 for event in iter_with_timeout(&rx) {
564 for path in event.paths {
565 a_file1_encountered =
566 a_file1_encountered || (path == a_file1 || path == a_file1.canonicalize()?);
567 b_file1_encountered =
568 b_file1_encountered || (path == b_file1 || path == b_file1.canonicalize()?);
569 }
570 if a_file1_encountered && b_file1_encountered {
571 break;
572 }
573 }
574 assert!(a_file1_encountered, "Did not receive event of {a_file1:?}");
575 assert!(b_file1_encountered, "Did not receive event of {b_file1:?}");
576
577 // stop watching a
578 {
579 let mut watcher_paths = watcher.paths_mut();
580 watcher_paths.remove(&dir_a)?;
581 watcher_paths.commit()?;
582 }
583
584 // create file2 in both a and b
585 let a_file2 = dir_a.join("file2");
586 let b_file2 = dir_b.join("file2");
587 fs::write(&a_file2, b"Lorem ipsum")?;
588 fs::write(&b_file2, b"Lorem ipsum")?;
589
590 // wait for the create event of b/file2 only
591 for event in iter_with_timeout(&rx) {
592 for path in event.paths {
593 assert!(
594 path != a_file2 || path != a_file2.canonicalize()?,
595 "Event of {a_file2:?} should not be received"
596 );
597 if path == b_file2 || path == b_file2.canonicalize()? {
598 return Ok(());
599 }
600 }
601 }
602 panic!("Did not receive the event of {b_file2:?}");
603 }
604
605 #[test]
606 fn create_file() {
607 let tmpdir = testdir();
608 let (mut watcher, mut rx) = recommended_channel();
609 watcher.watch_recursively(&tmpdir);
610
611 let path = tmpdir.path().join("entry");
612 std::fs::File::create_new(&path).expect("create");
613
614 rx.wait_unordered([expected(path).create()]);
615 }
616
617 #[test]
618 fn create_dir() {
619 let tmpdir = testdir();
620 let (mut watcher, mut rx) = recommended_channel();
621 watcher.watch_recursively(&tmpdir);
622
623 let path = tmpdir.path().join("entry");
624 std::fs::create_dir(&path).expect("create");
625
626 rx.wait_unordered([expected(path).create()]);
627 }
628
629 #[test]
630 fn modify_file() {
631 let tmpdir = testdir();
632 let (mut watcher, mut rx) = recommended_channel();
633
634 let path = tmpdir.path().join("entry");
635 std::fs::File::create_new(&path).expect("create");
636
637 watcher.watch_recursively(&tmpdir);
638 std::fs::write(&path, b"123").expect("write");
639
640 rx.wait_unordered([expected(path).modify()]);
641 }
642
643 #[test]
644 fn remove_file() {
645 let tmpdir = testdir();
646 let (mut watcher, mut rx) = recommended_channel();
647
648 let path = tmpdir.path().join("entry");
649 std::fs::File::create_new(&path).expect("create");
650
651 watcher.watch_recursively(&tmpdir);
652 std::fs::remove_file(&path).expect("remove");
653
654 rx.wait_unordered([expected(path).remove()]);
655 }
656}