notify-8.2.0/.cargo_vcs_info.json0000644000000001440000000000100123520ustar { "git": { "sha1": "a1d7c2d8f80786679d58ec6d5986a1d4278bc8cf" }, "path_in_vcs": "notify" }notify-8.2.0/.gitignore000064400000000000000000000001061046102023000131300ustar 00000000000000/target /Cargo.lock .*.sw* tests/last-fails tests/last-run.log .cargo notify-8.2.0/Cargo.toml0000644000000057760000000000100103700ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.77" name = "notify" version = "8.2.0" authors = [ "Félix Saparelli ", "Daniel Faust ", "Aron Heinecke ", ] description = "Cross-platform filesystem notification library" homepage = "https://github.com/notify-rs/notify" documentation = "https://docs.rs/notify" readme = "README.md" keywords = [ "events", "filesystem", "notify", "watch", ] categories = ["filesystem"] license = "CC0-1.0" repository = "https://github.com/notify-rs/notify.git" [dependencies.crossbeam-channel] version = "0.5.0" optional = true [dependencies.flume] version = "0.11.1" optional = true [dependencies.libc] version = "0.2.4" [dependencies.log] version = "0.4.17" [dependencies.notify-types] version = "2.0.0" [dependencies.walkdir] version = "2.4.0" [dev-dependencies.insta] version = "1.34.0" [dev-dependencies.nix] version = "0.29.0" [dev-dependencies.serde_json] version = "1.0.39" [dev-dependencies.tempfile] version = "3.10.0" [features] default = ["macos_fsevent"] macos_fsevent = ["fsevent-sys"] macos_kqueue = [ "kqueue", "mio", ] serde = ["notify-types/serde"] serialization-compat-6 = ["notify-types/serialization-compat-6"] [target."cfg(any(target_os=\"freebsd\", target_os=\"openbsd\", target_os = \"netbsd\", target_os = \"dragonflybsd\", target_os = \"ios\"))".dependencies.kqueue] version = "1.1.1" [target."cfg(any(target_os=\"freebsd\", target_os=\"openbsd\", target_os = \"netbsd\", target_os = \"dragonflybsd\", target_os = \"ios\"))".dependencies.mio] version = "1.0" features = ["os-ext"] [target."cfg(any(target_os=\"linux\", target_os=\"android\"))".dependencies.inotify] version = "0.11.0" default-features = false [target."cfg(any(target_os=\"linux\", target_os=\"android\"))".dependencies.mio] version = "1.0" features = ["os-ext"] [target."cfg(target_os = \"windows\")".dev-dependencies.trash] version = "5.2.2" [target."cfg(target_os=\"macos\")".dependencies.bitflags] version = "2.7.0" [target."cfg(target_os=\"macos\")".dependencies.fsevent-sys] version = "4.0.0" optional = true [target."cfg(target_os=\"macos\")".dependencies.kqueue] version = "1.1.1" optional = true [target."cfg(target_os=\"macos\")".dependencies.mio] version = "1.0" features = ["os-ext"] optional = true [target."cfg(windows)".dependencies.windows-sys] version = "0.60.1" features = [ "Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO", ] notify-8.2.0/Cargo.toml.orig000064400000000000000000000036101046102023000140320ustar 00000000000000[package] name = "notify" version = "8.2.0" description = "Cross-platform filesystem notification library" documentation = "https://docs.rs/notify" readme = "../README.md" license = "CC0-1.0" keywords = ["events", "filesystem", "notify", "watch"] categories = ["filesystem"] authors = [ "Félix Saparelli ", "Daniel Faust ", "Aron Heinecke " ] rust-version.workspace = true edition.workspace = true homepage.workspace = true repository.workspace = true [features] default = ["macos_fsevent"] serde = ["notify-types/serde"] macos_kqueue = ["kqueue", "mio"] macos_fsevent = ["fsevent-sys"] serialization-compat-6 = ["notify-types/serialization-compat-6"] [dependencies] notify-types.workspace = true crossbeam-channel = { workspace = true, optional = true } flume = { workspace = true, optional = true } libc.workspace = true log.workspace = true walkdir.workspace = true [target.'cfg(any(target_os="linux", target_os="android"))'.dependencies] inotify = { workspace = true, default-features = false } mio.workspace = true [target.'cfg(target_os="macos")'.dependencies] bitflags.workspace = true fsevent-sys = { workspace = true, optional = true } kqueue = { workspace = true, optional = true } mio = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = ["Win32_System_Threading", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_WindowsProgramming", "Win32_System_IO"] } [target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd", target_os = "ios"))'.dependencies] kqueue.workspace = true mio.workspace = true [dev-dependencies] serde_json.workspace = true tempfile.workspace = true nix.workspace = true insta.workspace = true [target.'cfg(target_os = "windows")'.dev-dependencies] trash.workspace = true notify-8.2.0/LICENSE-CC0000064400000000000000000000155371046102023000125260ustar 00000000000000Creative Commons CC0 1.0 Universal <> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. <> Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. notify-8.2.0/README.md000064400000000000000000000073151046102023000124300ustar 00000000000000# Notify [![» Crate](https://flat.badgen.net/crates/v/notify)][crate] [![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][notify-docs] [![» CI](https://flat.badgen.net/github/checks/notify-rs/notify/main)][build] [![» Downloads](https://flat.badgen.net/crates/d/notify)][crate] [![» Conduct](https://flat.badgen.net/badge/contributor/covenant/5e0d73)][coc] [![» Public Domain](https://flat.badgen.net/badge/license/CC0-1.0/purple)][cc0] _Cross-platform filesystem notification library for Rust._ - [Notify Documentation][notify-docs] - [Notify Types Documentation][notify-types-docs] - [Mini Debouncer Documentation][debouncer-mini-docs] - [Full Debouncer Documentation][debouncer-full-docs] - [File ID][file-id-docs] - [Examples][examples] - [Changelog][changelog] - [Upgrading notify from v4](UPGRADING_V4_TO_V5.md) - Minimum supported Rust version: **1.77** As used by: [alacritty], [cargo watch], [cobalt], [deno], [docket], [mdBook], [rust-analyzer], [watchexec], [watchfiles], [xi-editor], and others. (Looking for desktop notifications instead? Have a look at [notify-rust] or [alert-after]!) ## Platforms - Linux / Android: inotify - macOS: FSEvents or kqueue, see features - Windows: ReadDirectoryChangesW - iOS / FreeBSD / NetBSD / OpenBSD / DragonflyBSD: kqueue - All platforms: polling ## License notify is licensed under the [CC Zero 1.0][cc0]. notify-types is licensed under the [MIT] or [Apache-2.0][apache] license. notify-debouncer-mini is licensed under the [MIT] or [Apache-2.0][apache] license. notify-debouncer-full is licensed under the [MIT] or [Apache-2.0][apache] license. file-id is licensed under the [MIT] or [Apache-2.0][apache] license. ## Origins Inspired by Go's [fsnotify] and Node.js's [Chokidar], born out of need for [cargo watch], and general frustration at the non-existence of C/Rust cross-platform notify libraries. Originally created by [Félix Saparelli] and awesome [contributors]. [Chokidar]: https://github.com/paulmillr/chokidar [FileSystemEventSecurity]: https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/FileSystemEventSecurity/FileSystemEventSecurity.html [debouncer-full-docs]: https://docs.rs/notify-debouncer-full/latest/notify_debouncer_full/ [debouncer-mini-docs]: https://docs.rs/notify-debouncer-mini/latest/notify_debouncer_mini/ [Félix Saparelli]: https://passcod.name [alacritty]: https://github.com/jwilm/alacritty [alert-after]: https://github.com/frewsxcv/alert-after [build]: https://github.com/notify-rs/notify/actions [cargo watch]: https://github.com/passcod/cargo-watch [cc0]: ./notify/LICENSE-CC0 [MIT]: ./file-id/LICENSE-MIT [apache]: ./file-id/LICENSE-APACHE [changelog]: ./CHANGELOG.md [cobalt]: https://github.com/cobalt-org/cobalt.rs [coc]: http://contributor-covenant.org/version/1/4/ [contributors]: https://github.com/notify-rs/notify/graphs/contributors [crate]: https://crates.io/crates/notify [deno]: https://github.com/denoland/deno [docket]: https://iwillspeak.github.io/docket/ [notify-docs]: https://docs.rs/notify/latest/notify/ [notify-types-docs]: https://docs.rs/notify-types/latest/notify-types/ [file-id-docs]: https://docs.rs/file-id/latest/file_id/ [fsnotify]: https://github.com/fsnotify/fsnotify [handlebars-iron]: https://github.com/sunng87/handlebars-iron [hotwatch]: https://github.com/francesca64/hotwatch [mdBook]: https://github.com/rust-lang-nursery/mdBook [notify-rust]: https://github.com/hoodie/notify-rust [rust-analyzer]: https://github.com/rust-analyzer/rust-analyzer [serde]: https://serde.rs/ [watchexec]: https://github.com/mattgreen/watchexec [wiki]: https://github.com/notify-rs/notify/wiki [xi-editor]: https://xi-editor.io/ [watchfiles]: https://watchfiles.helpmanual.io/ [examples]: examples/ notify-8.2.0/src/config.rs000064400000000000000000000102601046102023000135440ustar 00000000000000//! Configuration types use std::time::Duration; /// Indicates whether only the provided directory or its sub-directories as well should be watched #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub enum RecursiveMode { /// Watch all sub-directories as well, including directories created after installing the watch Recursive, /// Watch only the provided directory NonRecursive, } impl RecursiveMode { pub(crate) fn is_recursive(&self) -> bool { match *self { RecursiveMode::Recursive => true, RecursiveMode::NonRecursive => false, } } } /// Watcher Backend configuration /// /// This contains multiple settings that may relate to only one specific backend, /// such as to correctly configure each backend regardless of what is selected during runtime. /// /// ```rust /// # use std::time::Duration; /// # use notify::Config; /// let config = Config::default() /// .with_poll_interval(Duration::from_secs(2)) /// .with_compare_contents(true); /// ``` /// /// Some options can be changed during runtime, others have to be set when creating the watcher backend. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Config { /// See [Config::with_poll_interval] poll_interval: Option, /// See [Config::with_compare_contents] compare_contents: bool, follow_symlinks: bool, } impl Config { /// For the [`PollWatcher`](crate::PollWatcher) backend. /// /// Interval between each re-scan attempt. This can be extremely expensive for large /// file trees so it is recommended to measure and tune accordingly. /// /// The default poll frequency is 30 seconds. /// /// This will enable automatic polling, overwriting [`with_manual_polling()`](Config::with_manual_polling). pub fn with_poll_interval(mut self, dur: Duration) -> Self { // TODO: v7.0 break signature to option self.poll_interval = Some(dur); self } /// Returns current setting pub fn poll_interval(&self) -> Option { // Changed Signature to Option self.poll_interval } /// For the [`PollWatcher`](crate::PollWatcher) backend. /// /// Disable automatic polling. Requires calling [`crate::PollWatcher::poll()`] manually. /// /// This will disable automatic polling, overwriting [`with_poll_interval()`](Config::with_poll_interval). pub fn with_manual_polling(mut self) -> Self { self.poll_interval = None; self } /// For the [`PollWatcher`](crate::PollWatcher) backend. /// /// Optional feature that will evaluate the contents of changed files to determine if /// they have indeed changed using a fast hashing algorithm. This is especially important /// for pseudo filesystems like those on Linux under /sys and /proc which are not obligated /// to respect any other filesystem norms such as modification timestamps, file sizes, etc. /// By enabling this feature, performance will be significantly impacted as all files will /// need to be read and hashed at each `poll_interval`. /// /// This can't be changed during runtime. Off by default. pub fn with_compare_contents(mut self, compare_contents: bool) -> Self { self.compare_contents = compare_contents; self } /// Returns current setting pub fn compare_contents(&self) -> bool { self.compare_contents } /// For the [INotifyWatcher](crate::INotifyWatcher), [KqueueWatcher](crate::KqueueWatcher), /// and [PollWatcher](crate::PollWatcher). /// /// Determine if symbolic links should be followed when recursively watching a directory. /// /// This can't be changed during runtime. On by default. pub fn with_follow_symlinks(mut self, follow_symlinks: bool) -> Self { self.follow_symlinks = follow_symlinks; self } /// Returns current setting pub fn follow_symlinks(&self) -> bool { self.follow_symlinks } } impl Default for Config { fn default() -> Self { Self { poll_interval: Some(Duration::from_secs(30)), compare_contents: false, follow_symlinks: true, } } } notify-8.2.0/src/error.rs000064400000000000000000000111761046102023000134370ustar 00000000000000//! Error types use crate::Config; use std::error::Error as StdError; use std::path::PathBuf; use std::result::Result as StdResult; use std::{self, fmt, io}; /// Type alias to use this library's `Error` type in a Result pub type Result = StdResult; /// Error kinds #[derive(Debug)] pub enum ErrorKind { /// Generic error /// /// May be used in cases where a platform specific error is mapped to this type, or for opaque /// internal errors. Generic(String), /// I/O errors. Io(io::Error), /// A path does not exist. PathNotFound, /// Attempted to remove a watch that does not exist. WatchNotFound, /// An invalid value was passed as runtime configuration. InvalidConfig(Config), /// Can't watch (more) files, limit on the total number of inotify watches reached MaxFilesWatch, } /// Notify error type. /// /// Errors are emitted either at creation time of a `Watcher`, or during the event stream. They /// range from kernel errors to filesystem errors to argument errors. /// /// Errors can be general, or they can be about specific paths or subtrees. In that later case, the /// error's `paths` field will be populated. #[derive(Debug)] pub struct Error { /// Kind of the error. pub kind: ErrorKind, /// Relevant paths to the error, if any. pub paths: Vec, } impl Error { /// Adds a path to the error. pub fn add_path(mut self, path: PathBuf) -> Self { self.paths.push(path); self } /// Replaces the paths for the error. pub fn set_paths(mut self, paths: Vec) -> Self { self.paths = paths; self } /// Creates a new Error with empty paths given its kind. pub fn new(kind: ErrorKind) -> Self { Self { kind, paths: Vec::new(), } } /// Creates a new generic Error from a message. pub fn generic(msg: &str) -> Self { Self::new(ErrorKind::Generic(msg.into())) } /// Creates a new i/o Error from a stdlib `io::Error`. pub fn io(err: io::Error) -> Self { Self::new(ErrorKind::Io(err)) } /// Similar to [`Error::io`], but specifically handles [`io::ErrorKind::NotFound`]. pub fn io_watch(err: io::Error) -> Self { if err.kind() == io::ErrorKind::NotFound { Self::path_not_found() } else { Self::io(err) } } /// Creates a new "path not found" error. pub fn path_not_found() -> Self { Self::new(ErrorKind::PathNotFound) } /// Creates a new "watch not found" error. pub fn watch_not_found() -> Self { Self::new(ErrorKind::WatchNotFound) } /// Creates a new "invalid config" error from the given `Config`. pub fn invalid_config(config: &Config) -> Self { Self::new(ErrorKind::InvalidConfig(*config)) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let error = match self.kind { ErrorKind::PathNotFound => "No path was found.".into(), ErrorKind::WatchNotFound => "No watch was found.".into(), ErrorKind::InvalidConfig(ref config) => format!("Invalid configuration: {:?}", config), ErrorKind::Generic(ref err) => err.clone(), ErrorKind::Io(ref err) => err.to_string(), ErrorKind::MaxFilesWatch => "OS file watch limit reached.".into(), }; if self.paths.is_empty() { write!(f, "{}", error) } else { write!(f, "{} about {:?}", error, self.paths) } } } impl StdError for Error { fn cause(&self) -> Option<&dyn StdError> { match self.kind { ErrorKind::Io(ref cause) => Some(cause), _ => None, } } } impl From for Error { fn from(err: io::Error) -> Self { Error::io(err) } } impl From> for Error { fn from(err: std::sync::mpsc::SendError) -> Self { Error::generic(&format!("internal channel disconnect: {:?}", err)) } } impl From for Error { fn from(err: std::sync::mpsc::RecvError) -> Self { Error::generic(&format!("internal channel disconnect: {:?}", err)) } } impl From> for Error { fn from(err: std::sync::PoisonError) -> Self { Error::generic(&format!("internal mutex poisoned: {:?}", err)) } } #[test] fn display_formatted_errors() { let expected = "Some error"; assert_eq!(expected, format!("{}", Error::generic(expected))); assert_eq!( expected, format!("{}", Error::io(io::Error::other(expected))) ); } notify-8.2.0/src/fsevent.rs000064400000000000000000000562441046102023000137650ustar 00000000000000//! Watcher implementation for Darwin's FSEvents API //! //! The FSEvents API provides a mechanism to notify clients about directories they ought to re-scan //! in order to keep their internal data structures up-to-date with respect to the true state of //! the file system. (For example, when files or directories are created, modified, or removed.) It //! sends these notifications "in bulk", possibly notifying the client of changes to several //! directories in a single callback. //! //! For more information see the [FSEvents API reference][ref]. //! //! TODO: document event translation //! //! [ref]: https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/ #![allow(non_upper_case_globals, dead_code)] use crate::event::*; use crate::{ unbounded, Config, Error, EventHandler, PathsMut, RecursiveMode, Result, Sender, Watcher, }; use fsevent_sys as fs; use fsevent_sys::core_foundation as cf; use std::collections::HashMap; use std::ffi::CStr; use std::fmt; use std::os::raw; use std::path::{Path, PathBuf}; use std::ptr; use std::sync::{Arc, Mutex}; use std::thread; bitflags::bitflags! { #[repr(C)] #[derive(Debug)] struct StreamFlags: u32 { const NONE = fs::kFSEventStreamEventFlagNone; const MUST_SCAN_SUBDIRS = fs::kFSEventStreamEventFlagMustScanSubDirs; const USER_DROPPED = fs::kFSEventStreamEventFlagUserDropped; const KERNEL_DROPPED = fs::kFSEventStreamEventFlagKernelDropped; const IDS_WRAPPED = fs::kFSEventStreamEventFlagEventIdsWrapped; const HISTORY_DONE = fs::kFSEventStreamEventFlagHistoryDone; const ROOT_CHANGED = fs::kFSEventStreamEventFlagRootChanged; const MOUNT = fs::kFSEventStreamEventFlagMount; const UNMOUNT = fs::kFSEventStreamEventFlagUnmount; const ITEM_CREATED = fs::kFSEventStreamEventFlagItemCreated; const ITEM_REMOVED = fs::kFSEventStreamEventFlagItemRemoved; const INODE_META_MOD = fs::kFSEventStreamEventFlagItemInodeMetaMod; const ITEM_RENAMED = fs::kFSEventStreamEventFlagItemRenamed; const ITEM_MODIFIED = fs::kFSEventStreamEventFlagItemModified; const FINDER_INFO_MOD = fs::kFSEventStreamEventFlagItemFinderInfoMod; const ITEM_CHANGE_OWNER = fs::kFSEventStreamEventFlagItemChangeOwner; const ITEM_XATTR_MOD = fs::kFSEventStreamEventFlagItemXattrMod; const IS_FILE = fs::kFSEventStreamEventFlagItemIsFile; const IS_DIR = fs::kFSEventStreamEventFlagItemIsDir; const IS_SYMLINK = fs::kFSEventStreamEventFlagItemIsSymlink; const OWN_EVENT = fs::kFSEventStreamEventFlagOwnEvent; const IS_HARDLINK = fs::kFSEventStreamEventFlagItemIsHardlink; const IS_LAST_HARDLINK = fs::kFSEventStreamEventFlagItemIsLastHardlink; const ITEM_CLONED = fs::kFSEventStreamEventFlagItemCloned; } } /// FSEvents-based `Watcher` implementation pub struct FsEventWatcher { paths: cf::CFMutableArrayRef, since_when: fs::FSEventStreamEventId, latency: cf::CFTimeInterval, flags: fs::FSEventStreamCreateFlags, event_handler: Arc>, runloop: Option<(cf::CFRunLoopRef, thread::JoinHandle<()>)>, recursive_info: HashMap, } impl fmt::Debug for FsEventWatcher { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("FsEventWatcher") .field("paths", &self.paths) .field("since_when", &self.since_when) .field("latency", &self.latency) .field("flags", &self.flags) .field("event_handler", &Arc::as_ptr(&self.event_handler)) .field("runloop", &self.runloop) .field("recursive_info", &self.recursive_info) .finish() } } // CFMutableArrayRef is a type alias to *mut libc::c_void, so FsEventWatcher is not Send/Sync // automatically. It's Send because the pointer is not used in other threads. unsafe impl Send for FsEventWatcher {} // It's Sync because all methods that change the mutable state use `&mut self`. unsafe impl Sync for FsEventWatcher {} fn translate_flags(flags: StreamFlags, precise: bool) -> Vec { let mut evs = Vec::new(); // «Denotes a sentinel event sent to mark the end of the "historical" events // sent as a result of specifying a `sinceWhen` value in the FSEvents.Create // call that created this event stream. After invoking the client's callback // with all the "historical" events that occurred before now, the client's // callback will be invoked with an event where the HistoryDone flag is set. // The client should ignore the path supplied in this callback.» // — https://www.mbsplugins.eu/FSEventsNextEvent.shtml // // As a result, we just stop processing here and return an empty vec, which // will ignore this completely and not emit any Events whatsoever. if flags.contains(StreamFlags::HISTORY_DONE) { return evs; } // FSEvents provides two possible hints as to why events were dropped, // however documentation on what those mean is scant, so we just pass them // through in the info attr field. The intent is clear enough, and the // additional information is provided if the user wants it. if flags.contains(StreamFlags::MUST_SCAN_SUBDIRS) { let e = Event::new(EventKind::Other).set_flag(Flag::Rescan); evs.push(if flags.contains(StreamFlags::USER_DROPPED) { e.set_info("rescan: user dropped") } else if flags.contains(StreamFlags::KERNEL_DROPPED) { e.set_info("rescan: kernel dropped") } else { e }); } // In imprecise mode, let's not even bother parsing the kind of the event // except for the above very special events. if !precise { evs.push(Event::new(EventKind::Any)); return evs; } // This is most likely a rename or a removal. We assume rename but may want // to figure out if it was a removal some way later (TODO). To denote the // special nature of the event, we add an info string. if flags.contains(StreamFlags::ROOT_CHANGED) { evs.push( Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::From))) .set_info("root changed"), ); } // A path was mounted at the event path; we treat that as a create. if flags.contains(StreamFlags::MOUNT) { evs.push(Event::new(EventKind::Create(CreateKind::Other)).set_info("mount")); } // A path was unmounted at the event path; we treat that as a remove. if flags.contains(StreamFlags::UNMOUNT) { evs.push(Event::new(EventKind::Remove(RemoveKind::Other)).set_info("mount")); } if flags.contains(StreamFlags::ITEM_CREATED) { evs.push(if flags.contains(StreamFlags::IS_DIR) { Event::new(EventKind::Create(CreateKind::Folder)) } else if flags.contains(StreamFlags::IS_FILE) { Event::new(EventKind::Create(CreateKind::File)) } else { let e = Event::new(EventKind::Create(CreateKind::Other)); if flags.contains(StreamFlags::IS_SYMLINK) { e.set_info("is: symlink") } else if flags.contains(StreamFlags::IS_HARDLINK) { e.set_info("is: hardlink") } else if flags.contains(StreamFlags::ITEM_CLONED) { e.set_info("is: clone") } else { Event::new(EventKind::Create(CreateKind::Any)) } }); } if flags.contains(StreamFlags::ITEM_REMOVED) { evs.push(if flags.contains(StreamFlags::IS_DIR) { Event::new(EventKind::Remove(RemoveKind::Folder)) } else if flags.contains(StreamFlags::IS_FILE) { Event::new(EventKind::Remove(RemoveKind::File)) } else { let e = Event::new(EventKind::Remove(RemoveKind::Other)); if flags.contains(StreamFlags::IS_SYMLINK) { e.set_info("is: symlink") } else if flags.contains(StreamFlags::IS_HARDLINK) { e.set_info("is: hardlink") } else if flags.contains(StreamFlags::ITEM_CLONED) { e.set_info("is: clone") } else { Event::new(EventKind::Remove(RemoveKind::Any)) } }); } // FSEvents provides no mechanism to associate the old and new sides of a // rename event. if flags.contains(StreamFlags::ITEM_RENAMED) { evs.push(Event::new(EventKind::Modify(ModifyKind::Name( RenameMode::Any, )))); } // This is only described as "metadata changed", but it may be that it's // only emitted for some more precise subset of events... if so, will need // amending, but for now we have an Any-shaped bucket to put it in. if flags.contains(StreamFlags::INODE_META_MOD) { evs.push(Event::new(EventKind::Modify(ModifyKind::Metadata( MetadataKind::Any, )))); } if flags.contains(StreamFlags::FINDER_INFO_MOD) { evs.push( Event::new(EventKind::Modify(ModifyKind::Metadata(MetadataKind::Other))) .set_info("meta: finder info"), ); } if flags.contains(StreamFlags::ITEM_CHANGE_OWNER) { evs.push(Event::new(EventKind::Modify(ModifyKind::Metadata( MetadataKind::Ownership, )))); } if flags.contains(StreamFlags::ITEM_XATTR_MOD) { evs.push(Event::new(EventKind::Modify(ModifyKind::Metadata( MetadataKind::Extended, )))); } // This is specifically described as a data change, which we take to mean // is a content change. if flags.contains(StreamFlags::ITEM_MODIFIED) { evs.push(Event::new(EventKind::Modify(ModifyKind::Data( DataChange::Content, )))); } if flags.contains(StreamFlags::OWN_EVENT) { for ev in &mut evs { *ev = std::mem::take(ev).set_process_id(std::process::id()); } } evs } struct StreamContextInfo { event_handler: Arc>, recursive_info: HashMap, } // Free the context when the stream created by `FSEventStreamCreate` is released. extern "C" fn release_context(info: *const libc::c_void) { // Safety: // - The [documentation] for `FSEventStreamContext` states that `release` is only // called when the stream is deallocated, so it is safe to convert `info` back into a // box and drop it. // // [docs]: https://developer.apple.com/documentation/coreservices/fseventstreamcontext?language=objc unsafe { drop(Box::from_raw( info as *const StreamContextInfo as *mut StreamContextInfo, )); } } extern "C" { /// Indicates whether the run loop is waiting for an event. fn CFRunLoopIsWaiting(runloop: cf::CFRunLoopRef) -> cf::Boolean; } struct FsEventPathsMut<'a>(&'a mut FsEventWatcher); impl<'a> FsEventPathsMut<'a> { fn new(watcher: &'a mut FsEventWatcher) -> Self { watcher.stop(); Self(watcher) } } impl PathsMut for FsEventPathsMut<'_> { fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.0.append_path(path, recursive_mode) } fn remove(&mut self, path: &Path) -> Result<()> { self.0.remove_path(path) } fn commit(self: Box) -> Result<()> { // ignore return error: may be empty path list let _ = self.0.run(); Ok(()) } } impl FsEventWatcher { fn from_event_handler(event_handler: Arc>) -> Result { Ok(FsEventWatcher { paths: unsafe { cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks) }, since_when: fs::kFSEventStreamEventIdSinceNow, latency: 0.0, flags: fs::kFSEventStreamCreateFlagFileEvents | fs::kFSEventStreamCreateFlagNoDefer, event_handler, runloop: None, recursive_info: HashMap::new(), }) } fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.stop(); let result = self.append_path(path, recursive_mode); // ignore return error: may be empty path list let _ = self.run(); result } fn unwatch_inner(&mut self, path: &Path) -> Result<()> { self.stop(); let result = self.remove_path(path); // ignore return error: may be empty path list let _ = self.run(); result } #[inline] fn is_running(&self) -> bool { self.runloop.is_some() } fn stop(&mut self) { if !self.is_running() { return; } if let Some((runloop, thread_handle)) = self.runloop.take() { unsafe { let runloop = runloop as *mut raw::c_void; while CFRunLoopIsWaiting(runloop) == 0 { thread::yield_now(); } cf::CFRunLoopStop(runloop); } // Wait for the thread to shut down. thread_handle.join().expect("thread to shut down"); } } fn remove_path(&mut self, path: &Path) -> Result<()> { let str_path = path.to_str().unwrap(); unsafe { let mut err: cf::CFErrorRef = ptr::null_mut(); let cf_path = cf::str_path_to_cfstring_ref(str_path, &mut err); if cf_path.is_null() { cf::CFRelease(err as cf::CFRef); return Err(Error::watch_not_found().add_path(path.into())); } let mut to_remove = Vec::new(); for idx in 0..cf::CFArrayGetCount(self.paths) { let item = cf::CFArrayGetValueAtIndex(self.paths, idx); if cf::CFStringCompare(item, cf_path, cf::kCFCompareCaseInsensitive) == cf::kCFCompareEqualTo { to_remove.push(idx); } } cf::CFRelease(cf_path); for idx in to_remove.iter().rev() { cf::CFArrayRemoveValueAtIndex(self.paths, *idx); } } let p = if let Ok(canonicalized_path) = path.canonicalize() { canonicalized_path } else { path.to_owned() }; match self.recursive_info.remove(&p) { Some(_) => Ok(()), None => Err(Error::watch_not_found()), } } // https://github.com/thibaudgg/rb-fsevent/blob/master/ext/fsevent_watch/main.c fn append_path(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { if !path.exists() { return Err(Error::path_not_found().add_path(path.into())); } let canonical_path = path.to_path_buf().canonicalize()?; let str_path = path.to_str().unwrap(); unsafe { let mut err: cf::CFErrorRef = ptr::null_mut(); let cf_path = cf::str_path_to_cfstring_ref(str_path, &mut err); if cf_path.is_null() { // Most likely the directory was deleted, or permissions changed, // while the above code was running. cf::CFRelease(err as cf::CFRef); return Err(Error::path_not_found().add_path(path.into())); } cf::CFArrayAppendValue(self.paths, cf_path); cf::CFRelease(cf_path); } self.recursive_info .insert(canonical_path, recursive_mode.is_recursive()); Ok(()) } fn run(&mut self) -> Result<()> { if unsafe { cf::CFArrayGetCount(self.paths) } == 0 { // TODO: Reconstruct and add paths to error return Err(Error::path_not_found()); } // We need to associate the stream context with our callback in order to propagate events // to the rest of the system. This will be owned by the stream, and will be freed when the // stream is closed. This means we will leak the context if we panic before reaching // `FSEventStreamRelease`. let context = Box::into_raw(Box::new(StreamContextInfo { event_handler: self.event_handler.clone(), recursive_info: self.recursive_info.clone(), })); let stream_context = fs::FSEventStreamContext { version: 0, info: context as *mut libc::c_void, retain: None, release: Some(release_context), copy_description: None, }; let stream = unsafe { fs::FSEventStreamCreate( cf::kCFAllocatorDefault, callback, &stream_context, self.paths, self.since_when, self.latency, self.flags, ) }; // Wrapper to help send CFRef types across threads. struct CFSendWrapper(cf::CFRef); // Safety: // - According to the Apple documentation, it's safe to move `CFRef`s across threads. // https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html unsafe impl Send for CFSendWrapper {} // move into thread let stream = CFSendWrapper(stream); // channel to pass runloop around let (rl_tx, rl_rx) = unbounded(); let thread_handle = thread::Builder::new() .name("notify-rs fsevents loop".to_string()) .spawn(move || { let _ = &stream; let stream = stream.0; unsafe { let cur_runloop = cf::CFRunLoopGetCurrent(); fs::FSEventStreamScheduleWithRunLoop( stream, cur_runloop, cf::kCFRunLoopDefaultMode, ); fs::FSEventStreamStart(stream); // the calling to CFRunLoopRun will be terminated by CFRunLoopStop call in drop() rl_tx .send(CFSendWrapper(cur_runloop)) .expect("Unable to send runloop to watcher"); cf::CFRunLoopRun(); fs::FSEventStreamStop(stream); // There are edge-cases, when many events are pending, // despite the stream being stopped, that the stream's // associated callback will be invoked. Purging events // is intended to prevent this. let event_id = fs::FSEventsGetCurrentEventId(); let device = fs::FSEventStreamGetDeviceBeingWatched(stream); fs::FSEventsPurgeEventsForDeviceUpToEventId(device, event_id); fs::FSEventStreamInvalidate(stream); fs::FSEventStreamRelease(stream); } })?; // block until runloop has been sent self.runloop = Some((rl_rx.recv().unwrap().0, thread_handle)); Ok(()) } fn configure_raw_mode(&mut self, _config: Config, tx: Sender>) { tx.send(Ok(false)) .expect("configuration channel disconnect"); } } extern "C" fn callback( stream_ref: fs::FSEventStreamRef, info: *mut libc::c_void, num_events: libc::size_t, // size_t numEvents event_paths: *mut libc::c_void, // void *eventPaths event_flags: *const fs::FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] event_ids: *const fs::FSEventStreamEventId, // const FSEventStreamEventId eventIds[] ) { unsafe { callback_impl( stream_ref, info, num_events, event_paths, event_flags, event_ids, ) } } unsafe fn callback_impl( _stream_ref: fs::FSEventStreamRef, info: *mut libc::c_void, num_events: libc::size_t, // size_t numEvents event_paths: *mut libc::c_void, // void *eventPaths event_flags: *const fs::FSEventStreamEventFlags, // const FSEventStreamEventFlags eventFlags[] _event_ids: *const fs::FSEventStreamEventId, // const FSEventStreamEventId eventIds[] ) { let event_paths = event_paths as *const *const libc::c_char; let info = info as *const StreamContextInfo; let event_handler = &(*info).event_handler; for p in 0..num_events { let path = CStr::from_ptr(*event_paths.add(p)) .to_str() .expect("Invalid UTF8 string."); let path = PathBuf::from(path); let flag = *event_flags.add(p); let flag = StreamFlags::from_bits(flag).unwrap_or_else(|| { panic!("Unable to decode StreamFlags: {}", flag); }); let mut handle_event = false; for (p, r) in &(*info).recursive_info { if path.starts_with(p) { if *r || &path == p { handle_event = true; break; } else if let Some(parent_path) = path.parent() { if parent_path == p { handle_event = true; break; } } } } if !handle_event { continue; } log::trace!("FSEvent: path = `{}`, flag = {:?}", path.display(), flag); for ev in translate_flags(flag, true).into_iter() { // TODO: precise let ev = ev.add_path(path.clone()); let mut event_handler = event_handler.lock().expect("lock not to be poisoned"); event_handler.handle_event(Ok(ev)); } } } impl Watcher for FsEventWatcher { /// Create a new watcher. fn new(event_handler: F, _config: Config) -> Result { Self::from_event_handler(Arc::new(Mutex::new(event_handler))) } fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.watch_inner(path, recursive_mode) } fn paths_mut<'me>(&'me mut self) -> Box { Box::new(FsEventPathsMut::new(self)) } fn unwatch(&mut self, path: &Path) -> Result<()> { self.unwatch_inner(path) } fn configure(&mut self, config: Config) -> Result { let (tx, rx) = unbounded(); self.configure_raw_mode(config, tx); rx.recv()? } fn kind() -> crate::WatcherKind { crate::WatcherKind::Fsevent } } impl Drop for FsEventWatcher { fn drop(&mut self) { self.stop(); unsafe { cf::CFRelease(self.paths); } } } #[test] fn test_fsevent_watcher_drop() { use super::*; use std::time::Duration; let dir = tempfile::tempdir().unwrap(); let (tx, rx) = std::sync::mpsc::channel(); { let mut watcher = FsEventWatcher::new(tx, Default::default()).unwrap(); watcher.watch(dir.path(), RecursiveMode::Recursive).unwrap(); thread::sleep(Duration::from_millis(2000)); println!("is running -> {}", watcher.is_running()); thread::sleep(Duration::from_millis(1000)); watcher.unwatch(dir.path()).unwrap(); println!("is running -> {}", watcher.is_running()); } thread::sleep(Duration::from_millis(1000)); for res in rx { let e = res.unwrap(); println!("debug => {:?} {:?}", e.kind, e.paths); } println!("in test: {} works", file!()); } #[test] fn test_steam_context_info_send_and_sync() { fn check_send() {} check_send::(); } notify-8.2.0/src/inotify.rs000064400000000000000000000703071046102023000137700ustar 00000000000000//! Watcher implementation for the inotify Linux API //! //! The inotify API provides a mechanism for monitoring filesystem events. Inotify can be used to //! monitor individual files, or to monitor directories. When a directory is monitored, inotify //! will return events for the directory itself, and for files inside the directory. use super::event::*; use super::{Config, Error, ErrorKind, EventHandler, RecursiveMode, Result, Watcher}; use crate::{bounded, unbounded, BoundSender, Receiver, Sender}; use inotify as inotify_sys; use inotify_sys::{EventMask, Inotify, WatchDescriptor, WatchMask}; use std::collections::HashMap; use std::env; use std::ffi::OsStr; use std::fs::metadata; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread; use walkdir::WalkDir; const INOTIFY: mio::Token = mio::Token(0); const MESSAGE: mio::Token = mio::Token(1); // The EventLoop will set up a mio::Poll and use it to wait for the following: // // - messages telling it what to do // // - events telling it that something has happened on one of the watched files. struct EventLoop { running: bool, poll: mio::Poll, event_loop_waker: Arc, event_loop_tx: Sender, event_loop_rx: Receiver, inotify: Option, event_handler: Box, /// PathBuf -> (WatchDescriptor, WatchMask, is_recursive, is_dir) watches: HashMap, paths: HashMap, rename_event: Option, follow_links: bool, } /// Watcher implementation based on inotify #[derive(Debug)] pub struct INotifyWatcher { channel: Sender, waker: Arc, } enum EventLoopMsg { AddWatch(PathBuf, RecursiveMode, Sender>), RemoveWatch(PathBuf, Sender>), Shutdown, Configure(Config, BoundSender>), } #[inline] fn add_watch_by_event( path: &PathBuf, event: &inotify_sys::Event<&OsStr>, watches: &HashMap, add_watches: &mut Vec, ) { if event.mask.contains(EventMask::ISDIR) { if let Some(parent_path) = path.parent() { if let Some(&(_, _, is_recursive, _)) = watches.get(parent_path) { if is_recursive { add_watches.push(path.to_owned()); } } } } } #[inline] fn remove_watch_by_event( path: &PathBuf, watches: &HashMap, remove_watches: &mut Vec, ) { if watches.contains_key(path) { remove_watches.push(path.to_owned()); } } impl EventLoop { pub fn new( inotify: Inotify, event_handler: Box, follow_links: bool, ) -> Result { let (event_loop_tx, event_loop_rx) = unbounded::(); let poll = mio::Poll::new()?; let event_loop_waker = Arc::new(mio::Waker::new(poll.registry(), MESSAGE)?); let inotify_fd = inotify.as_raw_fd(); let mut evented_inotify = mio::unix::SourceFd(&inotify_fd); poll.registry() .register(&mut evented_inotify, INOTIFY, mio::Interest::READABLE)?; let event_loop = EventLoop { running: true, poll, event_loop_waker, event_loop_tx, event_loop_rx, inotify: Some(inotify), event_handler, watches: HashMap::new(), paths: HashMap::new(), rename_event: None, follow_links, }; Ok(event_loop) } // Run the event loop. pub fn run(self) { let _ = thread::Builder::new() .name("notify-rs inotify loop".to_string()) .spawn(|| self.event_loop_thread()); } fn event_loop_thread(mut self) { let mut events = mio::Events::with_capacity(16); loop { // Wait for something to happen. match self.poll.poll(&mut events, None) { Err(ref e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => { // System call was interrupted, we will retry // TODO: Not covered by tests (to reproduce likely need to setup signal handlers) } Err(e) => panic!("poll failed: {}", e), Ok(()) => {} } // Process whatever happened. for event in &events { self.handle_event(event); } // Stop, if we're done. if !self.running { break; } } } // Handle a single event. fn handle_event(&mut self, event: &mio::event::Event) { match event.token() { MESSAGE => { // The channel is readable - handle messages. self.handle_messages() } INOTIFY => { // inotify has something to tell us. self.handle_inotify() } _ => unreachable!(), } } fn handle_messages(&mut self) { while let Ok(msg) = self.event_loop_rx.try_recv() { match msg { EventLoopMsg::AddWatch(path, recursive_mode, tx) => { let _ = tx.send(self.add_watch(path, recursive_mode.is_recursive(), true)); } EventLoopMsg::RemoveWatch(path, tx) => { let _ = tx.send(self.remove_watch(path, false)); } EventLoopMsg::Shutdown => { let _ = self.remove_all_watches(); if let Some(inotify) = self.inotify.take() { let _ = inotify.close(); } self.running = false; break; } EventLoopMsg::Configure(config, tx) => { self.configure_raw_mode(config, tx); } } } } fn configure_raw_mode(&mut self, _config: Config, tx: BoundSender>) { tx.send(Ok(false)) .expect("configuration channel disconnected"); } fn handle_inotify(&mut self) { let mut add_watches = Vec::new(); let mut remove_watches = Vec::new(); if let Some(ref mut inotify) = self.inotify { let mut buffer = [0; 1024]; // Read all buffers available. loop { match inotify.read_events(&mut buffer) { Ok(events) => { let mut num_events = 0; for event in events { log::trace!("inotify event: {event:?}"); num_events += 1; if event.mask.contains(EventMask::Q_OVERFLOW) { let ev = Ok(Event::new(EventKind::Other).set_flag(Flag::Rescan)); self.event_handler.handle_event(ev); } let path = match event.name { Some(name) => self.paths.get(&event.wd).map(|root| root.join(name)), None => self.paths.get(&event.wd).cloned(), }; let path = match path { Some(path) => path, None => { log::debug!("inotify event with unknown descriptor: {event:?}"); continue; } }; let mut evs = Vec::new(); if event.mask.contains(EventMask::MOVED_FROM) { remove_watch_by_event(&path, &self.watches, &mut remove_watches); let event = Event::new(EventKind::Modify(ModifyKind::Name( RenameMode::From, ))) .add_path(path.clone()) .set_tracker(event.cookie as usize); self.rename_event = Some(event.clone()); evs.push(event); } else if event.mask.contains(EventMask::MOVED_TO) { evs.push( Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::To))) .set_tracker(event.cookie as usize) .add_path(path.clone()), ); let trackers_match = self.rename_event.as_ref().and_then(|e| e.tracker()) == Some(event.cookie as usize); if trackers_match { let rename_event = self.rename_event.take().unwrap(); // unwrap is safe because `rename_event` must be set at this point evs.push( Event::new(EventKind::Modify(ModifyKind::Name( RenameMode::Both, ))) .set_tracker(event.cookie as usize) .add_some_path(rename_event.paths.first().cloned()) .add_path(path.clone()), ); } add_watch_by_event(&path, &event, &self.watches, &mut add_watches); } if event.mask.contains(EventMask::MOVE_SELF) { evs.push( Event::new(EventKind::Modify(ModifyKind::Name( RenameMode::From, ))) .add_path(path.clone()), ); // TODO stat the path and get to new path // - emit To and Both events // - change prefix for further events } if event.mask.contains(EventMask::CREATE) { evs.push( Event::new(EventKind::Create( if event.mask.contains(EventMask::ISDIR) { CreateKind::Folder } else { CreateKind::File }, )) .add_path(path.clone()), ); add_watch_by_event(&path, &event, &self.watches, &mut add_watches); } if event.mask.contains(EventMask::DELETE) { evs.push( Event::new(EventKind::Remove( if event.mask.contains(EventMask::ISDIR) { RemoveKind::Folder } else { RemoveKind::File }, )) .add_path(path.clone()), ); remove_watch_by_event(&path, &self.watches, &mut remove_watches); } if event.mask.contains(EventMask::DELETE_SELF) { let remove_kind = match self.watches.get(&path) { Some(&(_, _, _, true)) => RemoveKind::Folder, Some(&(_, _, _, false)) => RemoveKind::File, None => RemoveKind::Other, }; evs.push( Event::new(EventKind::Remove(remove_kind)) .add_path(path.clone()), ); remove_watch_by_event(&path, &self.watches, &mut remove_watches); } if event.mask.contains(EventMask::MODIFY) { evs.push( Event::new(EventKind::Modify(ModifyKind::Data( DataChange::Any, ))) .add_path(path.clone()), ); } if event.mask.contains(EventMask::CLOSE_WRITE) { evs.push( Event::new(EventKind::Access(AccessKind::Close( AccessMode::Write, ))) .add_path(path.clone()), ); } if event.mask.contains(EventMask::CLOSE_NOWRITE) { evs.push( Event::new(EventKind::Access(AccessKind::Close( AccessMode::Read, ))) .add_path(path.clone()), ); } if event.mask.contains(EventMask::ATTRIB) { evs.push( Event::new(EventKind::Modify(ModifyKind::Metadata( MetadataKind::Any, ))) .add_path(path.clone()), ); } if event.mask.contains(EventMask::OPEN) { evs.push( Event::new(EventKind::Access(AccessKind::Open( AccessMode::Any, ))) .add_path(path.clone()), ); } for ev in evs { self.event_handler.handle_event(Ok(ev)); } } // All events read. Break out. if num_events == 0 { break; } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { // No events read. Break out. break; } Err(e) => { self.event_handler.handle_event(Err(Error::io(e))); } } } } for path in remove_watches { self.remove_watch(path, true).ok(); } for path in add_watches { if let Err(add_watch_error) = self.add_watch(path, true, false) { // The handler should be notified if we have reached the limit. // Otherwise, the user might expect that a recursive watch // is continuing to work correctly, but it's not. if let ErrorKind::MaxFilesWatch = add_watch_error.kind { self.event_handler.handle_event(Err(add_watch_error)); // After that kind of a error we should stop adding watches, // because the limit has already reached and all next calls // will return us only the same error. break; } } } } fn add_watch(&mut self, path: PathBuf, is_recursive: bool, mut watch_self: bool) -> Result<()> { // If the watch is not recursive, or if we determine (by stat'ing the path to get its // metadata) that the watched path is not a directory, add a single path watch. if !is_recursive || !metadata(&path).map_err(Error::io_watch)?.is_dir() { return self.add_single_watch(path, false, true); } for entry in WalkDir::new(path) .follow_links(self.follow_links) .into_iter() .filter_map(filter_dir) { self.add_single_watch(entry.into_path(), is_recursive, watch_self)?; watch_self = false; } Ok(()) } fn add_single_watch( &mut self, path: PathBuf, is_recursive: bool, watch_self: bool, ) -> Result<()> { let mut watchmask = WatchMask::ATTRIB | WatchMask::CREATE | WatchMask::OPEN | WatchMask::DELETE | WatchMask::CLOSE_WRITE | WatchMask::MODIFY | WatchMask::MOVED_FROM | WatchMask::MOVED_TO; if watch_self { watchmask.insert(WatchMask::DELETE_SELF); watchmask.insert(WatchMask::MOVE_SELF); } if let Some(&(_, old_watchmask, _, _)) = self.watches.get(&path) { watchmask.insert(old_watchmask); watchmask.insert(WatchMask::MASK_ADD); } if let Some(ref mut inotify) = self.inotify { log::trace!("adding inotify watch: {}", path.display()); match inotify.watches().add(&path, watchmask) { Err(e) => { Err(if e.raw_os_error() == Some(libc::ENOSPC) { // do not report inotify limits as "no more space" on linux #266 Error::new(ErrorKind::MaxFilesWatch) } else if e.kind() == std::io::ErrorKind::NotFound { Error::new(ErrorKind::PathNotFound) } else { Error::io(e) } .add_path(path)) } Ok(w) => { watchmask.remove(WatchMask::MASK_ADD); let is_dir = metadata(&path).map_err(Error::io)?.is_dir(); self.watches .insert(path.clone(), (w.clone(), watchmask, is_recursive, is_dir)); self.paths.insert(w, path); Ok(()) } } } else { Ok(()) } } fn remove_watch(&mut self, path: PathBuf, remove_recursive: bool) -> Result<()> { match self.watches.remove(&path) { None => return Err(Error::watch_not_found().add_path(path)), Some((w, _, is_recursive, _)) => { if let Some(ref mut inotify) = self.inotify { let mut inotify_watches = inotify.watches(); log::trace!("removing inotify watch: {}", path.display()); inotify_watches .remove(w.clone()) .map_err(|e| Error::io(e).add_path(path.clone()))?; self.paths.remove(&w); if is_recursive || remove_recursive { let mut remove_list = Vec::new(); for (w, p) in &self.paths { if p.starts_with(&path) { inotify_watches .remove(w.clone()) .map_err(|e| Error::io(e).add_path(p.into()))?; self.watches.remove(p); remove_list.push(w.clone()); } } for w in remove_list { self.paths.remove(&w); } } } } } Ok(()) } fn remove_all_watches(&mut self) -> Result<()> { if let Some(ref mut inotify) = self.inotify { let mut inotify_watches = inotify.watches(); for (w, p) in &self.paths { inotify_watches .remove(w.clone()) .map_err(|e| Error::io(e).add_path(p.into()))?; } self.watches.clear(); self.paths.clear(); } Ok(()) } } /// return `DirEntry` when it is a directory fn filter_dir(e: walkdir::Result) -> Option { if let Ok(e) = e { if let Ok(metadata) = e.metadata() { if metadata.is_dir() { return Some(e); } } } None } impl INotifyWatcher { fn from_event_handler( event_handler: Box, follow_links: bool, ) -> Result { let inotify = Inotify::init()?; let event_loop = EventLoop::new(inotify, event_handler, follow_links)?; let channel = event_loop.event_loop_tx.clone(); let waker = event_loop.event_loop_waker.clone(); event_loop.run(); Ok(INotifyWatcher { channel, waker }) } fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; let (tx, rx) = unbounded(); let msg = EventLoopMsg::AddWatch(pb, recursive_mode, tx); // we expect the event loop to live and reply => unwraps must not panic self.channel.send(msg).unwrap(); self.waker.wake().unwrap(); rx.recv().unwrap() } fn unwatch_inner(&mut self, path: &Path) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; let (tx, rx) = unbounded(); let msg = EventLoopMsg::RemoveWatch(pb, tx); // we expect the event loop to live and reply => unwraps must not panic self.channel.send(msg).unwrap(); self.waker.wake().unwrap(); rx.recv().unwrap() } } impl Watcher for INotifyWatcher { /// Create a new watcher. fn new(event_handler: F, config: Config) -> Result { Self::from_event_handler(Box::new(event_handler), config.follow_symlinks()) } fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.watch_inner(path, recursive_mode) } fn unwatch(&mut self, path: &Path) -> Result<()> { self.unwatch_inner(path) } fn configure(&mut self, config: Config) -> Result { let (tx, rx) = bounded(1); self.channel.send(EventLoopMsg::Configure(config, tx))?; self.waker.wake()?; rx.recv()? } fn kind() -> crate::WatcherKind { crate::WatcherKind::Inotify } } impl Drop for INotifyWatcher { fn drop(&mut self) { // we expect the event loop to live => unwrap must not panic self.channel.send(EventLoopMsg::Shutdown).unwrap(); self.waker.wake().unwrap(); } } #[cfg(test)] mod tests { use std::{ sync::{atomic::AtomicBool, mpsc}, thread::available_parallelism, }; use super::*; #[test] fn inotify_watcher_is_send_and_sync() { fn check() {} check::(); } #[test] fn native_error_type_on_missing_path() { let mut watcher = INotifyWatcher::new(|_| {}, Config::default()).unwrap(); let result = watcher.watch( &PathBuf::from("/some/non/existant/path"), RecursiveMode::NonRecursive, ); assert!(matches!( result, Err(Error { paths: _, kind: ErrorKind::PathNotFound }) )) } /// Runs manually. /// /// * Save actual value of the limit: `MAX_USER_WATCHES=$(sysctl -n fs.inotify.max_user_watches)` /// * Run the test. /// * Set the limit to 0: `sudo sysctl fs.inotify.max_user_watches=0` while test is running /// * Wait for the test to complete /// * Restore the limit `sudo sysctl fs.inotify.max_user_watches=$MAX_USER_WATCHES` #[test] #[ignore = "requires changing sysctl fs.inotify.max_user_watches while test is running"] fn recursive_watch_calls_handler_if_creating_a_file_raises_max_files_watch() { use std::time::Duration; let tmpdir = tempfile::tempdir().unwrap(); let (tx, rx) = std::sync::mpsc::channel(); let (proc_changed_tx, proc_changed_rx) = std::sync::mpsc::channel(); let proc_path = Path::new("/proc/sys/fs/inotify/max_user_watches"); let mut watcher = INotifyWatcher::new( move |result: Result| match result { Ok(event) => { if event.paths.first().is_some_and(|path| path == proc_path) { proc_changed_tx.send(()).unwrap(); } } Err(e) => tx.send(e).unwrap(), }, Config::default(), ) .unwrap(); watcher .watch(tmpdir.path(), RecursiveMode::Recursive) .unwrap(); watcher .watch(proc_path, RecursiveMode::NonRecursive) .unwrap(); // give the time to set the limit proc_changed_rx .recv_timeout(Duration::from_secs(30)) .unwrap(); let child_dir = tmpdir.path().join("child"); std::fs::create_dir(child_dir).unwrap(); let result = rx.recv_timeout(Duration::from_millis(500)); assert!( matches!( &result, Ok(Error { kind: ErrorKind::MaxFilesWatch, paths: _, }) ), "expected {:?}, found: {:#?}", ErrorKind::MaxFilesWatch, result ); } /// https://github.com/notify-rs/notify/issues/678 #[test] fn race_condition_on_unwatch_and_pending_events_with_deleted_descriptor() { let tmpdir = tempfile::tempdir().expect("tmpdir"); let (tx, rx) = mpsc::channel(); let mut inotify = INotifyWatcher::new( move |e: Result| { let e = match e { Ok(e) if e.paths.is_empty() => e, Ok(_) | Err(_) => return, }; let _ = tx.send(e); }, Config::default(), ) .expect("inotify creation"); let dir_path = tmpdir.path(); let file_path = dir_path.join("foo"); std::fs::File::create(&file_path).unwrap(); let stop = Arc::new(AtomicBool::new(false)); let handles: Vec<_> = (0..available_parallelism().unwrap().get().max(4)) .map(|_| { let file_path = file_path.clone(); let stop = stop.clone(); thread::spawn(move || { while !stop.load(std::sync::atomic::Ordering::Relaxed) { let _ = std::fs::File::open(&file_path).unwrap(); } }) }) .collect(); let non_recursive = RecursiveMode::NonRecursive; for _ in 0..(handles.len() * 4) { inotify.watch(dir_path, non_recursive).unwrap(); inotify.unwatch(dir_path).unwrap(); } stop.store(true, std::sync::atomic::Ordering::Relaxed); handles .into_iter() .for_each(|handle| handle.join().ok().unwrap_or_default()); drop(inotify); let events: Vec<_> = rx.into_iter().map(|e| format!("{e:?}")).collect(); const LOG_LEN: usize = 10; let events_len = events.len(); assert!( events.is_empty(), "expected no events without path, but got {events_len}. first 10: {:#?}", &events[..LOG_LEN.min(events_len)] ); } } notify-8.2.0/src/kqueue.rs000064400000000000000000000437711046102023000136130ustar 00000000000000//! Watcher implementation for the kqueue API //! //! The kqueue() system call provides a generic method of notifying the user //! when an event happens or a condition holds, based on the results of small //! pieces of kernel code termed filters. use super::event::*; use super::{Config, Error, EventHandler, RecursiveMode, Result, Watcher}; use crate::{unbounded, Receiver, Sender}; use kqueue::{EventData, EventFilter, FilterFlag, Ident}; use std::collections::HashMap; use std::env; use std::fs::metadata; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::thread; use walkdir::WalkDir; const KQUEUE: mio::Token = mio::Token(0); const MESSAGE: mio::Token = mio::Token(1); // The EventLoop will set up a mio::Poll and use it to wait for the following: // // - messages telling it what to do // // - events telling it that something has happened on one of the watched files. struct EventLoop { running: bool, poll: mio::Poll, event_loop_waker: Arc, event_loop_tx: Sender, event_loop_rx: Receiver, kqueue: kqueue::Watcher, event_handler: Box, watches: HashMap, follow_symlinks: bool, } /// Watcher implementation based on inotify #[derive(Debug)] pub struct KqueueWatcher { channel: Sender, waker: Arc, } enum EventLoopMsg { AddWatch(PathBuf, RecursiveMode, Sender>), RemoveWatch(PathBuf, Sender>), Shutdown, } impl EventLoop { pub fn new( kqueue: kqueue::Watcher, event_handler: Box, follow_symlinks: bool, ) -> Result { let (event_loop_tx, event_loop_rx) = unbounded::(); let poll = mio::Poll::new()?; let event_loop_waker = Arc::new(mio::Waker::new(poll.registry(), MESSAGE)?); let kqueue_fd = kqueue.as_raw_fd(); let mut evented_kqueue = mio::unix::SourceFd(&kqueue_fd); poll.registry() .register(&mut evented_kqueue, KQUEUE, mio::Interest::READABLE)?; let event_loop = EventLoop { running: true, poll, event_loop_waker, event_loop_tx, event_loop_rx, kqueue, event_handler, watches: HashMap::new(), follow_symlinks, }; Ok(event_loop) } // Run the event loop. pub fn run(self) { let _ = thread::Builder::new() .name("notify-rs kqueue loop".to_string()) .spawn(|| self.event_loop_thread()); } fn event_loop_thread(mut self) { let mut events = mio::Events::with_capacity(16); loop { // Wait for something to happen. match self.poll.poll(&mut events, None) { Err(ref e) if matches!(e.kind(), std::io::ErrorKind::Interrupted) => { // System call was interrupted, we will retry // TODO: Not covered by tests (to reproduce likely need to setup signal handlers) } Err(e) => panic!("poll failed: {}", e), Ok(()) => {} } // Process whatever happened. for event in &events { self.handle_event(event); } // Stop, if we're done. if !self.running { break; } } } // Handle a single event. fn handle_event(&mut self, event: &mio::event::Event) { match event.token() { MESSAGE => { // The channel is readable - handle messages. self.handle_messages() } KQUEUE => { // inotify has something to tell us. self.handle_kqueue() } _ => unreachable!(), } } fn handle_messages(&mut self) { while let Ok(msg) = self.event_loop_rx.try_recv() { match msg { EventLoopMsg::AddWatch(path, recursive_mode, tx) => { let _ = tx.send(self.add_watch(path, recursive_mode.is_recursive())); } EventLoopMsg::RemoveWatch(path, tx) => { let _ = tx.send(self.remove_watch(path, false)); } EventLoopMsg::Shutdown => { self.running = false; break; } } } } fn handle_kqueue(&mut self) { let mut add_watches = Vec::new(); let mut remove_watches = Vec::new(); while let Some(event) = self.kqueue.poll(None) { log::trace!("kqueue event: {event:?}"); match event { kqueue::Event { data: EventData::Vnode(data), ident: Ident::Filename(_, path), } => { let path = PathBuf::from(path); let event = match data { /* TODO: Differentiate folders and files kqueue doesn't tell us if this was a file or a dir, so we could only emulate this inotify behavior if we keep track of all files and directories internally and then perform a lookup. */ kqueue::Vnode::Delete => { remove_watches.push(path.clone()); Ok(Event::new(EventKind::Remove(RemoveKind::Any)).add_path(path)) } // a write to a directory means that a new file was created in it, let's // figure out which file this was kqueue::Vnode::Write if path.is_dir() => { // find which file is new in the directory by comparing it with our // list of known watches std::fs::read_dir(&path) .map(|dir| { dir.filter_map(std::result::Result::ok) .map(|f| f.path()) .find(|f| !self.watches.contains_key(f)) }) .map(|file| { if let Some(file) = file { // watch this new file add_watches.push(file.clone()); Event::new(EventKind::Create(if file.is_dir() { CreateKind::Folder } else if file.is_file() { CreateKind::File } else { CreateKind::Other })) .add_path(file) } else { Event::new(EventKind::Modify(ModifyKind::Data( DataChange::Any, ))) .add_path(path) } }) .map_err(Into::into) } // data was written to this file kqueue::Vnode::Write => Ok(Event::new(EventKind::Modify( ModifyKind::Data(DataChange::Any), )) .add_path(path)), /* Extend and Truncate are just different names for the same operation, extend is only used on FreeBSD, truncate everywhere else */ kqueue::Vnode::Extend | kqueue::Vnode::Truncate => Ok(Event::new( EventKind::Modify(ModifyKind::Data(DataChange::Size)), ) .add_path(path)), /* this kevent has the same problem as the delete kevent. The only way i can think of providing "better" event with more information is to do the diff our self, while this maybe do able of delete. In this case it would somewhat expensive to keep track and compare ever peace of metadata for every file */ kqueue::Vnode::Attrib => Ok(Event::new(EventKind::Modify( ModifyKind::Metadata(MetadataKind::Any), )) .add_path(path)), /* The link count on a file changed => subdirectory created or delete. */ kqueue::Vnode::Link => { // As we currently don't have a solution that would allow us // to only add/remove the new/delete directory and that dosn't include a // possible race condition. On possible solution would be to // create a `HashMap>` which would // include every directory and this content add the time of // adding it to kqueue. While this should allow us to do the // diff and only add/remove the files necessary. This would // also introduce a race condition, where multiple files could // all ready be remove from the directory, and we could get out // of sync. // So for now, until we find a better solution, let remove and // readd the whole directory. // This is a expensive operation, as we recursive through all // subdirectories. remove_watches.push(path.clone()); add_watches.push(path.clone()); Ok(Event::new(EventKind::Modify(ModifyKind::Any)).add_path(path)) } // Kqueue not provide us with the information necessary to provide // the new file name to the event. kqueue::Vnode::Rename => { remove_watches.push(path.clone()); Ok( Event::new(EventKind::Modify(ModifyKind::Name(RenameMode::Any))) .add_path(path), ) } // Access to the file was revoked via revoke(2) or the underlying file system was unmounted. kqueue::Vnode::Revoke => { remove_watches.push(path.clone()); Ok(Event::new(EventKind::Remove(RemoveKind::Any)).add_path(path)) } // On different BSD variants, different extra events may be present #[allow(unreachable_patterns)] _ => Ok(Event::new(EventKind::Other)), }; self.event_handler.handle_event(event); } // as we don't add any other EVFILTER to kqueue we should never get here kqueue::Event { ident: _, data: _ } => unreachable!(), } } for path in remove_watches { self.remove_watch(path, true).ok(); } for path in add_watches { self.add_watch(path, true).ok(); } } fn add_watch(&mut self, path: PathBuf, is_recursive: bool) -> Result<()> { // If the watch is not recursive, or if we determine (by stat'ing the path to get its // metadata) that the watched path is not a directory, add a single path watch. if !is_recursive || !metadata(&path).map_err(Error::io)?.is_dir() { self.add_single_watch(path, false)?; } else { for entry in WalkDir::new(path) .follow_links(self.follow_symlinks) .into_iter() { let entry = entry.map_err(map_walkdir_error)?; self.add_single_watch(entry.into_path(), is_recursive)?; } } // Only make a single `kevent` syscall to add all the watches. self.kqueue.watch()?; Ok(()) } /// Adds a single watch to the kqueue. /// /// The caller of this function must call `self.kqueue.watch()` afterwards to register the new watch. fn add_single_watch(&mut self, path: PathBuf, is_recursive: bool) -> Result<()> { let event_filter = EventFilter::EVFILT_VNODE; let filter_flags = FilterFlag::NOTE_DELETE | FilterFlag::NOTE_WRITE | FilterFlag::NOTE_EXTEND | FilterFlag::NOTE_ATTRIB | FilterFlag::NOTE_LINK | FilterFlag::NOTE_RENAME | FilterFlag::NOTE_REVOKE; log::trace!("adding kqueue watch: {}", path.display()); self.kqueue .add_filename(&path, event_filter, filter_flags) .map_err(|e| Error::io(e).add_path(path.clone()))?; self.watches.insert(path, is_recursive); Ok(()) } fn remove_watch(&mut self, path: PathBuf, remove_recursive: bool) -> Result<()> { log::trace!("removing kqueue watch: {}", path.display()); match self.watches.remove(&path) { None => return Err(Error::watch_not_found()), Some(is_recursive) => { if is_recursive || remove_recursive { for entry in WalkDir::new(path) .follow_links(self.follow_symlinks) .into_iter() { let p = entry.map_err(map_walkdir_error)?.into_path(); self.kqueue .remove_filename(&p, EventFilter::EVFILT_VNODE) .map_err(|e| Error::io(e).add_path(p))?; } } else { self.kqueue .remove_filename(&path, EventFilter::EVFILT_VNODE) .map_err(|e| Error::io(e).add_path(path.clone()))?; } self.kqueue.watch()?; } } Ok(()) } } fn map_walkdir_error(e: walkdir::Error) -> Error { if e.io_error().is_some() { // save to unwrap otherwise we whouldn't be in this branch Error::io(e.into_io_error().unwrap()) } else { Error::generic(&e.to_string()) } } impl KqueueWatcher { fn from_event_handler( event_handler: Box, follow_symlinks: bool, ) -> Result { let kqueue = kqueue::Watcher::new()?; let event_loop = EventLoop::new(kqueue, event_handler, follow_symlinks)?; let channel = event_loop.event_loop_tx.clone(); let waker = event_loop.event_loop_waker.clone(); event_loop.run(); Ok(KqueueWatcher { channel, waker }) } fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; let (tx, rx) = unbounded(); let msg = EventLoopMsg::AddWatch(pb, recursive_mode, tx); self.channel .send(msg) .map_err(|e| Error::generic(&e.to_string()))?; self.waker .wake() .map_err(|e| Error::generic(&e.to_string()))?; rx.recv() .unwrap() .map_err(|e| Error::generic(&e.to_string())) } fn unwatch_inner(&mut self, path: &Path) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; let (tx, rx) = unbounded(); let msg = EventLoopMsg::RemoveWatch(pb, tx); self.channel .send(msg) .map_err(|e| Error::generic(&e.to_string()))?; self.waker .wake() .map_err(|e| Error::generic(&e.to_string()))?; rx.recv() .unwrap() .map_err(|e| Error::generic(&e.to_string())) } } impl Watcher for KqueueWatcher { /// Create a new watcher. fn new(event_handler: F, config: Config) -> Result { Self::from_event_handler(Box::new(event_handler), config.follow_symlinks()) } fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.watch_inner(path, recursive_mode) } fn unwatch(&mut self, path: &Path) -> Result<()> { self.unwatch_inner(path) } fn kind() -> crate::WatcherKind { crate::WatcherKind::Kqueue } } impl Drop for KqueueWatcher { fn drop(&mut self) { // we expect the event loop to live => unwrap must not panic self.channel.send(EventLoopMsg::Shutdown).unwrap(); self.waker.wake().unwrap(); } } mod tests { use super::{Config, KqueueWatcher, RecursiveMode}; use crate::Watcher; use std::error::Error; use std::path::PathBuf; use std::result::Result; #[test] fn test_remove_recursive() -> Result<(), Box> { let path = PathBuf::from("src"); let mut watcher = KqueueWatcher::new(|event| println!("{:?}", event), Config::default())?; watcher.watch(&path, RecursiveMode::Recursive)?; let result = watcher.unwatch(&path); assert!( result.is_ok(), "unwatch yielded error: {}", result.unwrap_err() ); Ok(()) } } notify-8.2.0/src/lib.rs000064400000000000000000000523451046102023000130570ustar 00000000000000//! Cross-platform file system notification library //! //! # Installation //! //! ```toml //! [dependencies] //! notify = "8.1.0" //! ``` //! //! 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/) //! or [notify-debouncer-full](https://docs.rs/notify-debouncer-full/latest/notify_debouncer_full/). //! //! ## Features //! //! List of compilation features, see below for details //! //! - `serde` for serialization of events //! - `macos_fsevent` enabled by default, for fsevent backend on macos //! - `macos_kqueue` for kqueue backend on macos //! - `serialization-compat-6` restores the serialization behavior of notify 6, off by default //! //! ### Serde //! //! Events are serializable via [serde](https://serde.rs) if the `serde` feature is enabled: //! //! ```toml //! notify = { version = "8.1.0", features = ["serde"] } //! ``` //! //! # Known Problems //! //! ### Network filesystems //! //! Network mounted filesystems like NFS may not emit any events for notify to listen to. //! This applies especially to WSL programs watching windows paths ([issue #254](https://github.com/notify-rs/notify/issues/254)). //! //! A workaround is the [`PollWatcher`] backend. //! //! ### Docker with Linux on macOS M1 //! //! Docker on macOS M1 [throws](https://github.com/notify-rs/notify/issues/423) `Function not implemented (os error 38)`. //! You have to manually use the [`PollWatcher`], as the native backend isn't available inside the emulation. //! //! ### macOS, FSEvents and unowned files //! //! Due to the inner security model of FSEvents (see [FileSystemEventSecurity](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/FileSystemEventSecurity/FileSystemEventSecurity.html)), //! some events cannot be observed easily when trying to follow files that do not //! belong to you. In this case, reverting to the pollwatcher can fix the issue, //! with a slight performance cost. //! //! ### Editor Behaviour //! //! If you rely on precise events (Write/Delete/Create..), you will notice that the actual events //! can differ a lot between file editors. Some truncate the file on save, some create a new one and replace the old one. //! 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. //! //! ### Parent folder deletion //! //! 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`. //! See [here](https://github.com/notify-rs/notify/issues/403) for more details. //! //! ### Pseudo Filesystems like /proc, /sys //! //! Some filesystems like `/proc` and `/sys` on *nix do not emit change events or use correct file change dates. //! To circumvent that problem you can use the [`PollWatcher`] with the `compare_contents` option. //! //! ### Linux: Bad File Descriptor / No space left on device //! //! This may be the case of running into the max-files watched limits of your user or system. //! (Files also includes folders.) Note that for recursive watched folders each file and folder inside counts towards the limit. //! //! You may increase this limit in linux via //! ```sh //! sudo sysctl fs.inotify.max_user_instances=8192 # example number //! sudo sysctl fs.inotify.max_user_watches=524288 # example number //! sudo sysctl -p //! ``` //! //! Note that the [`PollWatcher`] is not restricted by this limitation, so it may be an alternative if your users can't increase the limit. //! //! ### Watching large directories //! //! When watching a very large amount of files, notify may fail to receive all events. //! 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). //! //! # Examples //! //! For more examples visit the [examples folder](https://github.com/notify-rs/notify/tree/main/examples) in the repository. //! //! ```rust //! use notify::{Event, RecursiveMode, Result, Watcher}; //! use std::{path::Path, sync::mpsc}; //! //! fn main() -> Result<()> { //! let (tx, rx) = mpsc::channel::>(); //! //! // Use recommended_watcher() to automatically select the best implementation //! // for your platform. The `EventHandler` passed to this constructor can be a //! // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or //! // another type the trait is implemented for. //! let mut watcher = notify::recommended_watcher(tx)?; //! //! // Add a path to be watched. All files and directories at that path and //! // below will be monitored for changes. //! # #[cfg(not(any( //! # target_os = "freebsd", //! # target_os = "openbsd", //! # target_os = "dragonfly", //! # target_os = "netbsd")))] //! # { // "." doesn't exist on BSD for some reason in CI //! watcher.watch(Path::new("."), RecursiveMode::Recursive)?; //! # } //! # #[cfg(any())] //! # { // don't run this in doctests, it blocks forever //! // Block forever, printing out events as they come in //! for res in rx { //! match res { //! Ok(event) => println!("event: {:?}", event), //! Err(e) => println!("watch error: {:?}", e), //! } //! } //! # } //! //! Ok(()) //! } //! ``` //! //! ## With different configurations //! //! It is possible to create several watchers with different configurations or implementations that //! all call the same event function. This can accommodate advanced behaviour or work around limits. //! //! ```rust //! # use notify::{RecursiveMode, Result, Watcher}; //! # use std::path::Path; //! # //! # fn main() -> Result<()> { //! fn event_fn(res: Result) { //! match res { //! Ok(event) => println!("event: {:?}", event), //! Err(e) => println!("watch error: {:?}", e), //! } //! } //! //! let mut watcher1 = notify::recommended_watcher(event_fn)?; //! // we will just use the same watcher kind again here //! let mut watcher2 = notify::recommended_watcher(event_fn)?; //! # #[cfg(not(any( //! # target_os = "freebsd", //! # target_os = "openbsd", //! # target_os = "dragonfly", //! # target_os = "netbsd")))] //! # { // "." doesn't exist on BSD for some reason in CI //! # watcher1.watch(Path::new("."), RecursiveMode::Recursive)?; //! # watcher2.watch(Path::new("."), RecursiveMode::Recursive)?; //! # } //! // dropping the watcher1/2 here (no loop etc) will end the program //! # //! # Ok(()) //! # } //! ``` #![deny(missing_docs)] pub use config::{Config, RecursiveMode}; pub use error::{Error, ErrorKind, Result}; pub use notify_types::event::{self, Event, EventKind}; use std::path::Path; pub(crate) type Receiver = std::sync::mpsc::Receiver; pub(crate) type Sender = std::sync::mpsc::Sender; #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] pub(crate) type BoundSender = std::sync::mpsc::SyncSender; #[inline] pub(crate) fn unbounded() -> (Sender, Receiver) { std::sync::mpsc::channel() } #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] #[inline] pub(crate) fn bounded(cap: usize) -> (BoundSender, Receiver) { std::sync::mpsc::sync_channel(cap) } #[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))] pub use crate::fsevent::FsEventWatcher; #[cfg(any(target_os = "linux", target_os = "android"))] pub use crate::inotify::INotifyWatcher; #[cfg(any( target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "ios", all(target_os = "macos", feature = "macos_kqueue") ))] pub use crate::kqueue::KqueueWatcher; pub use null::NullWatcher; pub use poll::PollWatcher; #[cfg(target_os = "windows")] pub use windows::ReadDirectoryChangesWatcher; #[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))] pub mod fsevent; #[cfg(any(target_os = "linux", target_os = "android"))] pub mod inotify; #[cfg(any( target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly", target_os = "netbsd", target_os = "ios", all(target_os = "macos", feature = "macos_kqueue") ))] pub mod kqueue; #[cfg(target_os = "windows")] pub mod windows; pub mod null; pub mod poll; mod config; mod error; /// The set of requirements for watcher event handling functions. /// /// # Example implementation /// /// ```no_run /// use notify::{Event, Result, EventHandler}; /// /// /// Prints received events /// struct EventPrinter; /// /// impl EventHandler for EventPrinter { /// fn handle_event(&mut self, event: Result) { /// if let Ok(event) = event { /// println!("Event: {:?}", event); /// } /// } /// } /// ``` pub trait EventHandler: Send + 'static { /// Handles an event. fn handle_event(&mut self, event: Result); } impl EventHandler for F where F: FnMut(Result) + Send + 'static, { fn handle_event(&mut self, event: Result) { (self)(event); } } #[cfg(feature = "crossbeam-channel")] impl EventHandler for crossbeam_channel::Sender> { fn handle_event(&mut self, event: Result) { let _ = self.send(event); } } #[cfg(feature = "flume")] impl EventHandler for flume::Sender> { fn handle_event(&mut self, event: Result) { let _ = self.send(event); } } impl EventHandler for std::sync::mpsc::Sender> { fn handle_event(&mut self, event: Result) { let _ = self.send(event); } } /// Watcher kind enumeration #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum WatcherKind { /// inotify backend (linux) Inotify, /// FS-Event backend (mac) Fsevent, /// KQueue backend (bsd,optionally mac) Kqueue, /// Polling based backend (fallback) PollWatcher, /// Windows backend ReadDirectoryChangesWatcher, /// Fake watcher for testing NullWatcher, } /// Providing methods for adding and removing paths to watch. /// /// `Box` is created by [`Watcher::paths_mut`]. See its documentation for more. pub trait PathsMut { /// Add a new path to watch. See [`Watcher::watch`] for more. fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>; /// Remove a path from watching. See [`Watcher::unwatch`] for more. fn remove(&mut self, path: &Path) -> Result<()>; /// Ensure added/removed paths are applied. /// /// The behaviour of dropping a [`PathsMut`] without calling [`commit`] is unspecified. /// The implementation is free to ignore the changes or not, and may leave the watcher in a started or stopped state. fn commit(self: Box) -> Result<()>; } /// Type that can deliver file activity notifications /// /// `Watcher` is implemented per platform using the best implementation available on that platform. /// In addition to such event driven implementations, a polling implementation is also provided /// that should work on any platform. pub trait Watcher { /// Create a new watcher with an initial Config. fn new(event_handler: F, config: config::Config) -> Result where Self: Sized; /// Begin watching a new path. /// /// If the `path` is a directory, `recursive_mode` will be evaluated. If `recursive_mode` is /// `RecursiveMode::Recursive` events will be delivered for all files in that tree. Otherwise /// only the directory and its immediate children will be watched. /// /// If the `path` is a file, `recursive_mode` will be ignored and events will be delivered only /// for the file. /// /// On some platforms, if the `path` is renamed or removed while being watched, behaviour may /// be unexpected. See discussions in [#165] and [#166]. If less surprising behaviour is wanted /// one may non-recursively watch the _parent_ directory as well and manage related events. /// /// [#165]: https://github.com/notify-rs/notify/issues/165 /// [#166]: https://github.com/notify-rs/notify/issues/166 fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>; /// Stop watching a path. /// /// # Errors /// /// Returns an error in the case that `path` has not been watched or if removing the watch /// fails. fn unwatch(&mut self, path: &Path) -> Result<()>; /// Add/remove paths to watch. /// /// 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. /// /// # Examples /// /// ``` /// # use notify::{Watcher, RecursiveMode, Result}; /// # use std::path::Path; /// # fn main() -> Result<()> { /// # let many_paths_to_add = vec![]; /// let mut watcher = notify::recommended_watcher(|_event| { /* event handler */ })?; /// let mut watcher_paths = watcher.paths_mut(); /// for path in many_paths_to_add { /// watcher_paths.add(path, RecursiveMode::Recursive)?; /// } /// watcher_paths.commit()?; /// # Ok(()) /// # } /// ``` fn paths_mut<'me>(&'me mut self) -> Box { struct DefaultPathsMut<'a, T: ?Sized>(&'a mut T); impl<'a, T: Watcher + ?Sized> PathsMut for DefaultPathsMut<'a, T> { fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.0.watch(path, recursive_mode) } fn remove(&mut self, path: &Path) -> Result<()> { self.0.unwatch(path) } fn commit(self: Box) -> Result<()> { Ok(()) } } Box::new(DefaultPathsMut(self)) } /// Configure the watcher at runtime. /// /// See the [`Config`](config/struct.Config.html) struct for all configuration options. /// /// # Returns /// /// - `Ok(true)` on success. /// - `Ok(false)` if the watcher does not support or implement the option. /// - `Err(notify::Error)` on failure. fn configure(&mut self, _option: Config) -> Result { Ok(false) } /// Returns the watcher kind, allowing to perform backend-specific tasks fn kind() -> WatcherKind where Self: Sized; } /// The recommended [`Watcher`] implementation for the current platform #[cfg(any(target_os = "linux", target_os = "android"))] pub type RecommendedWatcher = INotifyWatcher; /// The recommended [`Watcher`] implementation for the current platform #[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))] pub type RecommendedWatcher = FsEventWatcher; /// The recommended [`Watcher`] implementation for the current platform #[cfg(target_os = "windows")] pub type RecommendedWatcher = ReadDirectoryChangesWatcher; /// The recommended [`Watcher`] implementation for the current platform #[cfg(any( target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "ios", all(target_os = "macos", feature = "macos_kqueue") ))] pub type RecommendedWatcher = KqueueWatcher; /// The recommended [`Watcher`] implementation for the current platform #[cfg(not(any( target_os = "linux", target_os = "android", target_os = "macos", target_os = "windows", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "ios" )))] pub type RecommendedWatcher = PollWatcher; /// Convenience method for creating the [`RecommendedWatcher`] for the current platform. pub fn recommended_watcher(event_handler: F) -> Result where F: EventHandler, { // All recommended watchers currently implement `new`, so just call that. RecommendedWatcher::new(event_handler, Config::default()) } #[cfg(test)] mod tests { use std::{ fs, iter, time::{Duration, Instant}, }; use tempfile::tempdir; use super::*; #[test] fn test_object_safe() { let _watcher: &dyn Watcher = &NullWatcher; } #[test] fn test_debug_impl() { macro_rules! assert_debug_impl { ($t:ty) => {{ #[allow(dead_code)] trait NeedsDebug: std::fmt::Debug {} impl NeedsDebug for $t {} }}; } assert_debug_impl!(Config); assert_debug_impl!(Error); assert_debug_impl!(ErrorKind); assert_debug_impl!(NullWatcher); assert_debug_impl!(PollWatcher); assert_debug_impl!(RecommendedWatcher); assert_debug_impl!(RecursiveMode); assert_debug_impl!(WatcherKind); } fn iter_with_timeout(rx: &Receiver>) -> impl Iterator + '_ { // wait for up to 10 seconds for the events let deadline = Instant::now() + Duration::from_secs(10); iter::from_fn(move || { if Instant::now() >= deadline { return None; } Some( rx.recv_timeout(deadline - Instant::now()) .expect("did not receive expected event") .expect("received an error"), ) }) } #[test] fn integration() -> std::result::Result<(), Box> { let dir = tempdir()?; // set up the watcher let (tx, rx) = std::sync::mpsc::channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; watcher.watch(dir.path(), RecursiveMode::Recursive)?; // create a new file let file_path = dir.path().join("file.txt"); fs::write(&file_path, b"Lorem ipsum")?; println!("waiting for event at {}", file_path.display()); // wait for the create event, ignore all other events for event in iter_with_timeout(&rx) { if event.paths == vec![file_path.clone()] || event.paths == vec![file_path.canonicalize()?] { return Ok(()); } println!("unexpected event: {event:?}"); } panic!("did not receive expected event"); } #[test] #[cfg(target_os = "windows")] fn test_windows_trash_dir() -> std::result::Result<(), Box> { let dir = tempdir()?; let child_dir = dir.path().join("child"); fs::create_dir(&child_dir)?; let mut watcher = recommended_watcher(|_| { // Do something with the event })?; watcher.watch(&child_dir, RecursiveMode::NonRecursive)?; trash::delete(&child_dir)?; watcher.watch(dir.path(), RecursiveMode::NonRecursive)?; Ok(()) } #[test] fn test_paths_mut() -> std::result::Result<(), Box> { let dir = tempdir()?; let dir_a = dir.path().join("a"); let dir_b = dir.path().join("b"); fs::create_dir(&dir_a)?; fs::create_dir(&dir_b)?; let (tx, rx) = std::sync::mpsc::channel(); let mut watcher = RecommendedWatcher::new(tx, Config::default())?; // start watching a and b { let mut watcher_paths = watcher.paths_mut(); watcher_paths.add(&dir_a, RecursiveMode::Recursive)?; watcher_paths.add(&dir_b, RecursiveMode::Recursive)?; watcher_paths.commit()?; } // create file1 in both a and b let a_file1 = dir_a.join("file1"); let b_file1 = dir_b.join("file1"); fs::write(&a_file1, b"Lorem ipsum")?; fs::write(&b_file1, b"Lorem ipsum")?; // wait for create events of a/file1 and b/file1 let mut a_file1_encountered: bool = false; let mut b_file1_encountered: bool = false; for event in iter_with_timeout(&rx) { for path in event.paths { a_file1_encountered = a_file1_encountered || (path == a_file1 || path == a_file1.canonicalize()?); b_file1_encountered = b_file1_encountered || (path == b_file1 || path == b_file1.canonicalize()?); } if a_file1_encountered && b_file1_encountered { break; } } assert!(a_file1_encountered, "Did not receive event of {a_file1:?}"); assert!(b_file1_encountered, "Did not receive event of {b_file1:?}"); // stop watching a { let mut watcher_paths = watcher.paths_mut(); watcher_paths.remove(&dir_a)?; watcher_paths.commit()?; } // create file2 in both a and b let a_file2 = dir_a.join("file2"); let b_file2 = dir_b.join("file2"); fs::write(&a_file2, b"Lorem ipsum")?; fs::write(&b_file2, b"Lorem ipsum")?; // wait for the create event of b/file2 only for event in iter_with_timeout(&rx) { for path in event.paths { assert!( path != a_file2 || path != a_file2.canonicalize()?, "Event of {a_file2:?} should not be received" ); if path == b_file2 || path == b_file2.canonicalize()? { return Ok(()); } } } panic!("Did not receive the event of {b_file2:?}"); } } notify-8.2.0/src/null.rs000064400000000000000000000014631046102023000132560ustar 00000000000000//! Stub Watcher implementation #![allow(unused_variables)] use crate::Config; use super::{RecursiveMode, Result, Watcher}; use std::path::Path; /// Stub `Watcher` implementation /// /// Events are never delivered from this watcher. #[derive(Debug)] pub struct NullWatcher; impl Watcher for NullWatcher { fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { Ok(()) } fn unwatch(&mut self, path: &Path) -> Result<()> { Ok(()) } fn new(event_handler: F, config: Config) -> Result where Self: Sized, { Ok(NullWatcher) } fn configure(&mut self, config: Config) -> Result { Ok(false) } fn kind() -> crate::WatcherKind { crate::WatcherKind::NullWatcher } } notify-8.2.0/src/poll.rs000064400000000000000000000541671046102023000132630ustar 00000000000000//! Generic Watcher implementation based on polling //! //! Checks the `watch`ed paths periodically to detect changes. This implementation only uses //! Rust stdlib APIs and should work on all of the platforms it supports. use crate::{unbounded, Config, Error, EventHandler, Receiver, RecursiveMode, Sender, Watcher}; use std::{ collections::HashMap, path::{Path, PathBuf}, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, thread, time::Duration, }; /// Event sent for registered handlers on initial directory scans pub type ScanEvent = crate::Result; /// Handler trait for receivers of [`ScanEvent`]. /// Very much the same as [`EventHandler`], but including the Result. /// /// See the full example for more information. pub trait ScanEventHandler: Send + 'static { /// Handles an event. fn handle_event(&mut self, event: ScanEvent); } impl ScanEventHandler for F where F: FnMut(ScanEvent) + Send + 'static, { fn handle_event(&mut self, event: ScanEvent) { (self)(event); } } #[cfg(feature = "crossbeam-channel")] impl ScanEventHandler for crossbeam_channel::Sender { fn handle_event(&mut self, event: ScanEvent) { let _ = self.send(event); } } #[cfg(feature = "flume")] impl ScanEventHandler for flume::Sender { fn handle_event(&mut self, event: ScanEvent) { let _ = self.send(event); } } impl ScanEventHandler for std::sync::mpsc::Sender { fn handle_event(&mut self, event: ScanEvent) { let _ = self.send(event); } } impl ScanEventHandler for () { fn handle_event(&mut self, _event: ScanEvent) {} } use data::{DataBuilder, WatchData}; mod data { use crate::{ event::{CreateKind, DataChange, Event, EventKind, MetadataKind, ModifyKind, RemoveKind}, EventHandler, }; use std::{ cell::RefCell, collections::{hash_map::RandomState, HashMap}, fmt::{self, Debug}, fs::{self, File, Metadata}, hash::{BuildHasher, Hasher}, io::{self, Read}, path::{Path, PathBuf}, time::Instant, }; use walkdir::WalkDir; use super::ScanEventHandler; fn system_time_to_seconds(time: std::time::SystemTime) -> i64 { match time.duration_since(std::time::SystemTime::UNIX_EPOCH) { Ok(d) => d.as_secs() as i64, Err(e) => -(e.duration().as_secs() as i64), } } /// Builder for [`WatchData`] & [`PathData`]. pub(super) struct DataBuilder { emitter: EventEmitter, scan_emitter: Option>>, // TODO: May allow user setup their custom BuildHasher / BuildHasherDefault // in future. build_hasher: Option, // current timestamp for building Data. now: Instant, } impl DataBuilder { pub(super) fn new( event_handler: F, compare_content: bool, scan_emitter: Option, ) -> Self where F: EventHandler, G: ScanEventHandler, { let scan_emitter = match scan_emitter { None => None, Some(v) => { // workaround for a weird type resolution bug when directly going to dyn Trait let intermediate: Box> = Box::new(RefCell::new(v)); Some(intermediate) } }; Self { emitter: EventEmitter::new(event_handler), scan_emitter, build_hasher: compare_content.then(RandomState::default), now: Instant::now(), } } /// Update internal timestamp. pub(super) fn update_timestamp(&mut self) { self.now = Instant::now(); } /// Create [`WatchData`]. /// /// This function will return `Err(_)` if can not retrieve metadata from /// the path location. (e.g., not found). pub(super) fn build_watch_data( &self, root: PathBuf, is_recursive: bool, follow_symlinks: bool, ) -> Option { WatchData::new(self, root, is_recursive, follow_symlinks) } /// Create [`PathData`]. fn build_path_data(&self, meta_path: &MetaPath) -> PathData { PathData::new(self, meta_path) } } impl Debug for DataBuilder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("DataBuilder") .field("build_hasher", &self.build_hasher) .field("now", &self.now) .finish() } } #[derive(Debug)] pub(super) struct WatchData { // config part, won't change. root: PathBuf, is_recursive: bool, follow_symlinks: bool, // current status part. all_path_data: HashMap, } impl WatchData { /// Scan filesystem and create a new `WatchData`. /// /// # Side effect /// /// This function may send event by `data_builder.emitter`. fn new( data_builder: &DataBuilder, root: PathBuf, is_recursive: bool, follow_symlinks: bool, ) -> Option { // If metadata read error at `root` path, it will emit // a error event and stop to create the whole `WatchData`. // // QUESTION: inconsistent? // // When user try to *CREATE* a watch by `poll_watcher.watch(root, ..)`, // if `root` path hit an io error, then watcher will reject to // create this new watch. // // This may inconsistent with *POLLING* a watch. When watcher // continue polling, io error at root path will not delete // a existing watch. polling still working. // // So, consider a config file may not exists at first time but may // create after a while, developer cannot watch it. // // FIXME: Can we always allow to watch a path, even file not // found at this path? if let Err(e) = fs::metadata(&root) { data_builder.emitter.emit_io_err(e, Some(&root)); return None; } let all_path_data = Self::scan_all_path_data( data_builder, root.clone(), is_recursive, follow_symlinks, true, ) .collect(); Some(Self { root, is_recursive, follow_symlinks, all_path_data, }) } /// Rescan filesystem and update this `WatchData`. /// /// # Side effect /// /// This function may emit event by `data_builder.emitter`. pub(super) fn rescan(&mut self, data_builder: &mut DataBuilder) { // scan current filesystem. for (path, new_path_data) in Self::scan_all_path_data( data_builder, self.root.clone(), self.is_recursive, self.follow_symlinks, false, ) { let old_path_data = self .all_path_data .insert(path.clone(), new_path_data.clone()); // emit event let event = PathData::compare_to_event(path, old_path_data.as_ref(), Some(&new_path_data)); if let Some(event) = event { data_builder.emitter.emit_ok(event); } } // scan for disappeared paths. let mut disappeared_paths = Vec::new(); for (path, path_data) in self.all_path_data.iter() { if path_data.last_check < data_builder.now { disappeared_paths.push(path.clone()); } } // remove disappeared paths for path in disappeared_paths { let old_path_data = self.all_path_data.remove(&path); // emit event let event = PathData::compare_to_event(path, old_path_data.as_ref(), None); if let Some(event) = event { data_builder.emitter.emit_ok(event); } } } /// Get all `PathData` by given configuration. /// /// # Side Effect /// /// This function may emit some IO Error events by `data_builder.emitter`. fn scan_all_path_data( data_builder: &'_ DataBuilder, root: PathBuf, is_recursive: bool, follow_symlinks: bool, // whether this is an initial scan, used only for events is_initial: bool, ) -> impl Iterator + '_ { log::trace!("rescanning {root:?}"); // WalkDir return only one entry if root is a file (not a folder), // so we can use single logic to do the both file & dir's jobs. // // See: https://docs.rs/walkdir/2.0.1/walkdir/struct.WalkDir.html#method.new WalkDir::new(root) .follow_links(follow_symlinks) .max_depth(Self::dir_scan_depth(is_recursive)) .into_iter() .filter_map(|entry_res| match entry_res { Ok(entry) => Some(entry), Err(err) => { log::warn!("walkdir error scanning {err:?}"); if let Some(io_error) = err.io_error() { // clone an io::Error, so we have to create a new one. let new_io_error = io::Error::new(io_error.kind(), err.to_string()); data_builder.emitter.emit_io_err(new_io_error, err.path()); } else { let crate_err = crate::Error::new(crate::ErrorKind::Generic(err.to_string())); data_builder.emitter.emit(Err(crate_err)); } None } }) .filter_map(move |entry| match entry.metadata() { Ok(metadata) => { let path = entry.into_path(); if is_initial { // emit initial scans if let Some(ref emitter) = data_builder.scan_emitter { emitter.borrow_mut().handle_event(Ok(path.clone())); } } let meta_path = MetaPath::from_parts_unchecked(path, metadata); let data_path = data_builder.build_path_data(&meta_path); Some((meta_path.into_path(), data_path)) } Err(e) => { // emit event. let path = entry.into_path(); data_builder.emitter.emit_io_err(e, Some(path)); None } }) } fn dir_scan_depth(is_recursive: bool) -> usize { if is_recursive { usize::MAX } else { 1 } } } /// Stored data for a one path locations. /// /// See [`WatchData`] for more detail. #[derive(Debug, Clone)] struct PathData { /// File updated time. mtime: i64, /// Content's hash value, only available if user request compare file /// contents and read successful. hash: Option, /// Checked time. last_check: Instant, } impl PathData { /// Create a new `PathData`. fn new(data_builder: &DataBuilder, meta_path: &MetaPath) -> PathData { let metadata = meta_path.metadata(); PathData { mtime: metadata.modified().map_or(0, system_time_to_seconds), hash: data_builder .build_hasher .as_ref() .filter(|_| metadata.is_file()) .and_then(|build_hasher| { Self::get_content_hash(build_hasher, meta_path.path()).ok() }), last_check: data_builder.now, } } /// Get hash value for the data content in given file `path`. fn get_content_hash(build_hasher: &RandomState, path: &Path) -> io::Result { let mut hasher = build_hasher.build_hasher(); let mut file = File::open(path)?; let mut buf = [0; 512]; loop { let n = match file.read(&mut buf) { Ok(0) => break, Ok(len) => len, Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, Err(e) => return Err(e), }; hasher.write(&buf[..n]); } Ok(hasher.finish()) } /// Get [`Event`] by compare two optional [`PathData`]. fn compare_to_event

( path: P, old: Option<&PathData>, new: Option<&PathData>, ) -> Option where P: Into, { match (old, new) { (Some(old), Some(new)) => { if new.mtime > old.mtime { Some(EventKind::Modify(ModifyKind::Metadata( MetadataKind::WriteTime, ))) } else if new.hash != old.hash { Some(EventKind::Modify(ModifyKind::Data(DataChange::Any))) } else { None } } (None, Some(_new)) => Some(EventKind::Create(CreateKind::Any)), (Some(_old), None) => Some(EventKind::Remove(RemoveKind::Any)), (None, None) => None, } .map(|event_kind| Event::new(event_kind).add_path(path.into())) } } /// Compose path and its metadata. /// /// This data structure designed for make sure path and its metadata can be /// transferred in consistent way, and may avoid some duplicated /// `fs::metadata()` function call in some situations. #[derive(Debug)] pub(super) struct MetaPath { path: PathBuf, metadata: Metadata, } impl MetaPath { /// Create `MetaPath` by given parts. /// /// # Invariant /// /// User must make sure the input `metadata` are associated with `path`. fn from_parts_unchecked(path: PathBuf, metadata: Metadata) -> Self { Self { path, metadata } } fn path(&self) -> &Path { &self.path } fn metadata(&self) -> &Metadata { &self.metadata } fn into_path(self) -> PathBuf { self.path } } /// Thin wrapper for outer event handler, for easy to use. struct EventEmitter( // Use `RefCell` to make sure `emit()` only need shared borrow of self (&self). // Use `Box` to make sure EventEmitter is Sized. Box>, ); impl EventEmitter { fn new(event_handler: F) -> Self { Self(Box::new(RefCell::new(event_handler))) } /// Emit single event. fn emit(&self, event: crate::Result) { self.0.borrow_mut().handle_event(event); } /// Emit event. fn emit_ok(&self, event: Event) { self.emit(Ok(event)) } /// Emit io error event. fn emit_io_err(&self, err: E, path: Option

) where E: Into, P: Into, { let e = crate::Error::io(err.into()); if let Some(path) = path { self.emit(Err(e.add_path(path.into()))); } else { self.emit(Err(e)); } } } } /// Polling based `Watcher` implementation. /// /// By default scans through all files and checks for changed entries based on their change date. /// Can also be changed to perform file content change checks. /// /// See [Config] for more details. #[derive(Debug)] pub struct PollWatcher { watches: Arc>>, data_builder: Arc>, want_to_stop: Arc, /// channel to the poll loop /// currently used only for manual polling message_channel: Sender<()>, delay: Option, follow_sylinks: bool, } impl PollWatcher { /// Create a new [`PollWatcher`], configured as needed. pub fn new(event_handler: F, config: Config) -> crate::Result { Self::with_opt::<_, ()>(event_handler, config, None) } /// Actively poll for changes. Can be combined with a timeout of 0 to perform only manual polling. pub fn poll(&self) -> crate::Result<()> { self.message_channel .send(()) .map_err(|_| Error::generic("failed to send poll message"))?; Ok(()) } /// Create a new [`PollWatcher`] with an scan event handler. /// /// `scan_fallback` is called on the initial scan with all files seen by the pollwatcher. pub fn with_initial_scan( event_handler: F, config: Config, scan_callback: G, ) -> crate::Result { Self::with_opt(event_handler, config, Some(scan_callback)) } /// create a new [`PollWatcher`] with all options. fn with_opt( event_handler: F, config: Config, scan_callback: Option, ) -> crate::Result { let data_builder = DataBuilder::new(event_handler, config.compare_contents(), scan_callback); let (tx, rx) = unbounded(); let poll_watcher = PollWatcher { watches: Default::default(), data_builder: Arc::new(Mutex::new(data_builder)), want_to_stop: Arc::new(AtomicBool::new(false)), delay: config.poll_interval(), follow_sylinks: config.follow_symlinks(), message_channel: tx, }; poll_watcher.run(rx); Ok(poll_watcher) } fn run(&self, rx: Receiver<()>) { let watches = Arc::clone(&self.watches); let data_builder = Arc::clone(&self.data_builder); let want_to_stop = Arc::clone(&self.want_to_stop); let delay = self.delay; let _ = thread::Builder::new() .name("notify-rs poll loop".to_string()) .spawn(move || { loop { if want_to_stop.load(Ordering::SeqCst) { break; } // HINT: Make sure always lock in the same order to avoid deadlock. // // FIXME: inconsistent: some place mutex poison cause panic, // some place just ignore. if let (Ok(mut watches), Ok(mut data_builder)) = (watches.lock(), data_builder.lock()) { data_builder.update_timestamp(); let vals = watches.values_mut(); for watch_data in vals { watch_data.rescan(&mut data_builder); } } // TODO: v7.0 use delay - (Instant::now().saturating_duration_since(start)) if let Some(delay) = delay { let _ = rx.recv_timeout(delay); } else { let _ = rx.recv(); } } }); } /// Watch a path location. /// /// QUESTION: this function never return an Error, is it as intend? /// Please also consider the IO Error event problem. fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) { // HINT: Make sure always lock in the same order to avoid deadlock. // // FIXME: inconsistent: some place mutex poison cause panic, some place just ignore. if let (Ok(mut watches), Ok(mut data_builder)) = (self.watches.lock(), self.data_builder.lock()) { data_builder.update_timestamp(); let watch_data = data_builder.build_watch_data( path.to_path_buf(), recursive_mode.is_recursive(), self.follow_sylinks, ); // if create watch_data successful, add it to watching list. if let Some(watch_data) = watch_data { watches.insert(path.to_path_buf(), watch_data); } } } /// Unwatch a path. /// /// Return `Err(_)` if given path has't be monitored. fn unwatch_inner(&mut self, path: &Path) -> crate::Result<()> { // FIXME: inconsistent: some place mutex poison cause panic, some place just ignore. self.watches .lock() .unwrap() .remove(path) .map(|_| ()) .ok_or_else(crate::Error::watch_not_found) } } impl Watcher for PollWatcher { /// Create a new [`PollWatcher`]. fn new(event_handler: F, config: Config) -> crate::Result { Self::new(event_handler, config) } fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> crate::Result<()> { self.watch_inner(path, recursive_mode); Ok(()) } fn unwatch(&mut self, path: &Path) -> crate::Result<()> { self.unwatch_inner(path) } fn kind() -> crate::WatcherKind { crate::WatcherKind::PollWatcher } } impl Drop for PollWatcher { fn drop(&mut self) { self.want_to_stop.store(true, Ordering::Relaxed); } } #[test] fn poll_watcher_is_send_and_sync() { fn check() {} check::(); } notify-8.2.0/src/windows.rs000064400000000000000000000511041046102023000137730ustar 00000000000000#![allow(missing_docs)] //! Watcher implementation for Windows' directory management APIs //! //! For more information see the [ReadDirectoryChangesW reference][ref]. //! //! [ref]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa363950(v=vs.85).aspx use crate::{bounded, unbounded, BoundSender, Config, Receiver, Sender}; use crate::{event::*, WatcherKind}; use crate::{Error, EventHandler, RecursiveMode, Result, Watcher}; use std::alloc; use std::collections::HashMap; use std::env; use std::ffi::OsString; use std::os::raw::c_void; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::path::{Path, PathBuf}; use std::ptr; use std::slice; use std::sync::{Arc, Mutex}; use std::thread; use windows_sys::Win32::Foundation::{ CloseHandle, ERROR_ACCESS_DENIED, ERROR_OPERATION_ABORTED, ERROR_SUCCESS, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0, }; use windows_sys::Win32::Storage::FileSystem::{ CreateFileW, ReadDirectoryChangesW, FILE_ACTION_ADDED, FILE_ACTION_MODIFIED, FILE_ACTION_REMOVED, FILE_ACTION_RENAMED_NEW_NAME, FILE_ACTION_RENAMED_OLD_NAME, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED, FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_CREATION, FILE_NOTIFY_CHANGE_DIR_NAME, FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY, FILE_NOTIFY_CHANGE_SIZE, FILE_NOTIFY_INFORMATION, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, }; use windows_sys::Win32::System::Threading::{ CreateSemaphoreW, ReleaseSemaphore, WaitForSingleObjectEx, INFINITE, }; use windows_sys::Win32::System::IO::{CancelIo, OVERLAPPED}; const BUF_SIZE: u32 = 16384; #[derive(Clone)] struct ReadData { dir: PathBuf, // directory that is being watched file: Option, // if a file is being watched, this is its full path complete_sem: HANDLE, is_recursive: bool, } struct ReadDirectoryRequest { event_handler: Arc>, buffer: [u8; BUF_SIZE as usize], handle: HANDLE, data: ReadData, action_tx: Sender, } impl ReadDirectoryRequest { fn unwatch(&self) { let _ = self.action_tx.send(Action::Unwatch(self.data.dir.clone())); } } enum Action { Watch(PathBuf, RecursiveMode), Unwatch(PathBuf), Stop, Configure(Config, BoundSender>), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum MetaEvent { SingleWatchComplete, WatcherAwakened, } struct WatchState { dir_handle: HANDLE, complete_sem: HANDLE, } struct ReadDirectoryChangesServer { tx: Sender, rx: Receiver, event_handler: Arc>, meta_tx: Sender, cmd_tx: Sender>, watches: HashMap, wakeup_sem: HANDLE, } impl ReadDirectoryChangesServer { fn start( event_handler: Arc>, meta_tx: Sender, cmd_tx: Sender>, wakeup_sem: HANDLE, ) -> Sender { let (action_tx, action_rx) = unbounded(); // it is, in fact, ok to send the semaphore across threads let sem_temp = wakeup_sem as u64; let _ = thread::Builder::new() .name("notify-rs windows loop".to_string()) .spawn({ let tx = action_tx.clone(); move || { let wakeup_sem = sem_temp as HANDLE; let server = ReadDirectoryChangesServer { tx, rx: action_rx, event_handler, meta_tx, cmd_tx, watches: HashMap::new(), wakeup_sem, }; server.run(); } }); action_tx } fn run(mut self) { loop { // process all available actions first let mut stopped = false; while let Ok(action) = self.rx.try_recv() { match action { Action::Watch(path, recursive_mode) => { let res = self.add_watch(path, recursive_mode.is_recursive()); let _ = self.cmd_tx.send(res); } Action::Unwatch(path) => self.remove_watch(path), Action::Stop => { stopped = true; for ws in self.watches.values() { stop_watch(ws, &self.meta_tx); } break; } Action::Configure(config, tx) => { self.configure_raw_mode(config, tx); } } } if stopped { break; } unsafe { // wait with alertable flag so that the completion routine fires let waitres = WaitForSingleObjectEx(self.wakeup_sem, 100, 1); if waitres == WAIT_OBJECT_0 { let _ = self.meta_tx.send(MetaEvent::WatcherAwakened); } } } // we have to clean this up, since the watcher may be long gone unsafe { CloseHandle(self.wakeup_sem); } } fn add_watch(&mut self, path: PathBuf, is_recursive: bool) -> Result { // path must exist and be either a file or directory if !path.is_dir() && !path.is_file() { return Err( Error::generic("Input watch path is neither a file nor a directory.") .add_path(path), ); } let (watching_file, dir_target) = { if path.is_dir() { (false, path.clone()) } else { // emulate file watching by watching the parent directory (true, path.parent().unwrap().to_path_buf()) } }; let encoded_path: Vec = dir_target .as_os_str() .encode_wide() .chain(Some(0)) .collect(); let handle; unsafe { handle = CreateFileW( encoded_path.as_ptr(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, ptr::null_mut(), OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, ptr::null_mut(), ); if handle == INVALID_HANDLE_VALUE { return Err(if watching_file { Error::generic( "You attempted to watch a single file, but parent \ directory could not be opened.", ) .add_path(path) } else { // TODO: Call GetLastError for better error info? Error::path_not_found().add_path(path) }); } } let wf = if watching_file { Some(path.clone()) } else { None }; // every watcher gets its own semaphore to signal completion let semaphore = unsafe { CreateSemaphoreW(ptr::null_mut(), 0, 1, ptr::null_mut()) }; if semaphore.is_null() || semaphore == INVALID_HANDLE_VALUE { unsafe { CloseHandle(handle); } return Err(Error::generic("Failed to create semaphore for watch.").add_path(path)); } let rd = ReadData { dir: dir_target, file: wf, complete_sem: semaphore, is_recursive, }; let ws = WatchState { dir_handle: handle, complete_sem: semaphore, }; self.watches.insert(path.clone(), ws); start_read(&rd, self.event_handler.clone(), handle, self.tx.clone()); Ok(path) } fn remove_watch(&mut self, path: PathBuf) { if let Some(ws) = self.watches.remove(&path) { stop_watch(&ws, &self.meta_tx); } } fn configure_raw_mode(&mut self, _config: Config, tx: BoundSender>) { tx.send(Ok(false)) .expect("configuration channel disconnect"); } } fn stop_watch(ws: &WatchState, meta_tx: &Sender) { unsafe { let cio = CancelIo(ws.dir_handle); let ch = CloseHandle(ws.dir_handle); // have to wait for it, otherwise we leak the memory allocated for there read request if cio != 0 && ch != 0 { while WaitForSingleObjectEx(ws.complete_sem, INFINITE, 1) != WAIT_OBJECT_0 { // drain the apc queue, fix for https://github.com/notify-rs/notify/issues/287#issuecomment-801465550 } } CloseHandle(ws.complete_sem); } let _ = meta_tx.send(MetaEvent::SingleWatchComplete); } fn start_read( rd: &ReadData, event_handler: Arc>, handle: HANDLE, action_tx: Sender, ) { let request = Box::new(ReadDirectoryRequest { event_handler, handle, buffer: [0u8; BUF_SIZE as usize], data: rd.clone(), action_tx, }); let flags = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_SECURITY; let monitor_subdir = if request.data.file.is_none() && request.data.is_recursive { 1 } else { 0 }; unsafe { let overlapped = alloc::alloc_zeroed(alloc::Layout::new::()) as *mut OVERLAPPED; // When using callback based async requests, we are allowed to use the hEvent member // for our own purposes let request = Box::leak(request); (*overlapped).hEvent = request as *mut _ as _; // This is using an asynchronous call with a completion routine for receiving notifications // An I/O completion port would probably be more performant let ret = ReadDirectoryChangesW( handle, request.buffer.as_mut_ptr() as *mut c_void, BUF_SIZE, monitor_subdir, flags, &mut 0u32 as *mut u32, // not used for async reqs overlapped, Some(handle_event), ); if ret == 0 { // error reading. retransmute request memory to allow drop. // Because of the error, ownership of the `overlapped` alloc was not passed // over to `ReadDirectoryChangesW`. // So we can claim ownership back. let _overlapped = Box::from_raw(overlapped); let request = Box::from_raw(request); ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); } } } unsafe extern "system" fn handle_event( error_code: u32, _bytes_written: u32, overlapped: *mut OVERLAPPED, ) { let overlapped: Box = Box::from_raw(overlapped); let request: Box = Box::from_raw(overlapped.hEvent as *mut _); match error_code { ERROR_OPERATION_ABORTED => { // received when dir is unwatched or watcher is shutdown; return and let overlapped/request get drop-cleaned ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); return; } ERROR_ACCESS_DENIED => { // This could happen when the watched directory is deleted or trashed, first check if it's the case. // If so, unwatch the directory and return, otherwise, continue to handle the event. if !request.data.dir.exists() { request.unwatch(); ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); return; } } ERROR_SUCCESS => { // Success, continue to handle the event } _ => { // Some unidentified error occurred, log and unwatch the directory, then return. log::error!( "unknown error in ReadDirectoryChangesW for directory {}: {}", request.data.dir.display(), error_code ); request.unwatch(); ReleaseSemaphore(request.data.complete_sem, 1, ptr::null_mut()); return; } } // Get the next request queued up as soon as possible start_read( &request.data, request.event_handler.clone(), request.handle, request.action_tx, ); // The FILE_NOTIFY_INFORMATION struct has a variable length due to the variable length // string as its last member. Each struct contains an offset for getting the next entry in // the buffer. let mut cur_offset: *const u8 = request.buffer.as_ptr(); // In Wine, FILE_NOTIFY_INFORMATION structs are packed placed in the buffer; // they are aligned to 16bit (WCHAR) boundary instead of 32bit required by FILE_NOTIFY_INFORMATION. // Hence, we need to use `read_unaligned` here to avoid UB. let mut cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_INFORMATION); loop { // filename length is size in bytes, so / 2 let len = cur_entry.FileNameLength as usize / 2; let encoded_path: &[u16] = slice::from_raw_parts( cur_offset.offset(std::mem::offset_of!(FILE_NOTIFY_INFORMATION, FileName) as isize) as _, len, ); // prepend root to get a full path let path = request .data .dir .join(PathBuf::from(OsString::from_wide(encoded_path))); // if we are watching a single file, ignore the event unless the path is exactly // the watched file let skip = match request.data.file { None => false, Some(ref watch_path) => *watch_path != path, }; if !skip { log::trace!( "Event: path = `{}`, action = {:?}", path.display(), cur_entry.Action ); let newe = Event::new(EventKind::Any).add_path(path); fn emit_event(event_handler: &Mutex, res: Result) { if let Ok(mut guard) = event_handler.lock() { let f: &mut dyn EventHandler = &mut *guard; f.handle_event(res); } } let event_handler = |res| emit_event(&request.event_handler, res); if cur_entry.Action == FILE_ACTION_RENAMED_OLD_NAME { let mode = RenameMode::From; let kind = ModifyKind::Name(mode); let kind = EventKind::Modify(kind); let ev = newe.set_kind(kind); event_handler(Ok(ev)) } else { match cur_entry.Action { FILE_ACTION_RENAMED_NEW_NAME => { let kind = EventKind::Modify(ModifyKind::Name(RenameMode::To)); let ev = newe.set_kind(kind); event_handler(Ok(ev)); } FILE_ACTION_ADDED => { let kind = EventKind::Create(CreateKind::Any); let ev = newe.set_kind(kind); event_handler(Ok(ev)); } FILE_ACTION_REMOVED => { let kind = EventKind::Remove(RemoveKind::Any); let ev = newe.set_kind(kind); event_handler(Ok(ev)); } FILE_ACTION_MODIFIED => { let kind = EventKind::Modify(ModifyKind::Any); let ev = newe.set_kind(kind); event_handler(Ok(ev)); } _ => (), }; } } if cur_entry.NextEntryOffset == 0 { break; } cur_offset = cur_offset.offset(cur_entry.NextEntryOffset as isize); cur_entry = ptr::read_unaligned(cur_offset as *const FILE_NOTIFY_INFORMATION); } } /// Watcher implementation based on ReadDirectoryChanges #[derive(Debug)] pub struct ReadDirectoryChangesWatcher { tx: Sender, cmd_rx: Receiver>, wakeup_sem: HANDLE, } impl ReadDirectoryChangesWatcher { pub fn create( event_handler: Arc>, meta_tx: Sender, ) -> Result { let (cmd_tx, cmd_rx) = unbounded(); let wakeup_sem = unsafe { CreateSemaphoreW(ptr::null_mut(), 0, 1, ptr::null_mut()) }; if wakeup_sem.is_null() || wakeup_sem == INVALID_HANDLE_VALUE { return Err(Error::generic("Failed to create wakeup semaphore.")); } let action_tx = ReadDirectoryChangesServer::start(event_handler, meta_tx, cmd_tx, wakeup_sem); Ok(ReadDirectoryChangesWatcher { tx: action_tx, cmd_rx, wakeup_sem, }) } fn wakeup_server(&mut self) { // breaks the server out of its wait state. right now this is really just an optimization, // so that if you add a watch you don't block for 100ms in watch() while the // server sleeps. unsafe { ReleaseSemaphore(self.wakeup_sem, 1, ptr::null_mut()); } } fn send_action_require_ack(&mut self, action: Action, pb: &PathBuf) -> Result<()> { self.tx .send(action) .map_err(|_| Error::generic("Error sending to internal channel"))?; // wake 'em up, we don't want to wait around for the ack self.wakeup_server(); let ack_pb = self .cmd_rx .recv() .map_err(|_| Error::generic("Error receiving from command channel"))? .map_err(|e| Error::generic(&format!("Error in watcher: {:?}", e)))?; if pb.as_path() != ack_pb.as_path() { Err(Error::generic(&format!( "Expected ack for {:?} but got \ ack for {:?}", pb, ack_pb ))) } else { Ok(()) } } fn watch_inner(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; // path must exist and be either a file or directory if !pb.is_dir() && !pb.is_file() { return Err(Error::generic( "Input watch path is neither a file nor a directory.", )); } self.send_action_require_ack(Action::Watch(pb.clone(), recursive_mode), &pb) } fn unwatch_inner(&mut self, path: &Path) -> Result<()> { let pb = if path.is_absolute() { path.to_owned() } else { let p = env::current_dir().map_err(Error::io)?; p.join(path) }; let res = self .tx .send(Action::Unwatch(pb)) .map_err(|_| Error::generic("Error sending to internal channel")); self.wakeup_server(); res } } impl Watcher for ReadDirectoryChangesWatcher { fn new(event_handler: F, _config: Config) -> Result { // create dummy channel for meta event // TODO: determine the original purpose of this - can we remove it? let (meta_tx, _) = unbounded(); let event_handler = Arc::new(Mutex::new(event_handler)); Self::create(event_handler, meta_tx) } fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> { self.watch_inner(path, recursive_mode) } fn unwatch(&mut self, path: &Path) -> Result<()> { self.unwatch_inner(path) } fn configure(&mut self, config: Config) -> Result { let (tx, rx) = bounded(1); self.tx.send(Action::Configure(config, tx))?; rx.recv()? } fn kind() -> crate::WatcherKind { WatcherKind::ReadDirectoryChangesWatcher } } impl Drop for ReadDirectoryChangesWatcher { fn drop(&mut self) { let _ = self.tx.send(Action::Stop); // better wake it up self.wakeup_server(); } } // `ReadDirectoryChangesWatcher` is not Send/Sync because of the semaphore Handle. // As said elsewhere it's perfectly safe to send it across threads. unsafe impl Send for ReadDirectoryChangesWatcher {} // Because all public methods are `&mut self` it's also perfectly safe to share references. unsafe impl Sync for ReadDirectoryChangesWatcher {}