Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method for native watcher initialization with fallback #441

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,16 @@ path = "poll_sysfs.rs"
name = "watcher_kind"
path = "watcher_kind.rs"

[[example]]
name = "multiple_backends"
path = "multiple_backends.rs"

# specifically in its own sub folder
# to prevent cargo audit from complaining
#[[example]]
#name = "hot_reload_tide"
#name = "hot_reload_tide"

# fix invalid rust-analyzer warnings
# when changing notify but not updating the crates version
[patch.crates-io]
notify = { path = "../notify/" }
42 changes: 42 additions & 0 deletions examples/multiple_backends.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use notify::{RecursiveMode, Result, Watcher, Config};
use std::path::Path;

/// Initialize multiple backends via `recommended_watcher_fallback`
fn fallback_init() -> Result<()> {
fn event_fn(res: Result<notify::Event>) {
match res {
Ok(event) => println!("event: {:?}", event),
Err(e) => println!("watch error: {:?}", e),
}
}

let mut watcher1 = notify::recommended_watcher_fallback(event_fn, Config::default())?;
// we will just use the same watcher kind again here
let mut watcher2 = notify::recommended_watcher_fallback(event_fn, Config::default())?;
watcher1.watch(Path::new("."), RecursiveMode::Recursive)?;
watcher2.watch(Path::new("."), RecursiveMode::Recursive)?;
Ok(())
}

/// Initialize multiple backends via recommended_watcher
fn direct_init() -> Result<()> {
fn event_fn(res: Result<notify::Event>) {
match res {
Ok(event) => println!("event: {:?}", event),
Err(e) => println!("watch error: {:?}", e),
}
}

let mut watcher1 = notify::RecommendedWatcher::new(event_fn, Config::default())?;
// we will just use the same watcher kind again here
let mut watcher2 = notify::RecommendedWatcher::new(event_fn, Config::default())?;
watcher1.watch(Path::new("."), RecursiveMode::Recursive)?;
watcher2.watch(Path::new("."), RecursiveMode::Recursive)?;
Ok(())
}

fn main() -> Result<()> {
direct_init()?;
fallback_init()?;
Ok(())
}
108 changes: 107 additions & 1 deletion notify/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
//! 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.
//!
//! [recommended_watcher_fallback] works around this issue automatically.
//!
//! ### 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)),
Expand Down Expand Up @@ -282,6 +284,8 @@ pub enum WatcherKind {
ReadDirectoryChangesWatcher,
/// Fake watcher for testing
NullWatcher,
/// If invoked on [WatcherFallback] created via [recommended_watcher_fallback]
UnknownFallback,
}

/// Type that can deliver file activity notifications
Expand Down Expand Up @@ -339,15 +343,23 @@ pub trait Watcher {
}

/// The recommended `Watcher` implementation for the current platform
///
/// Please use [recommended_watcher_fallback] if you want to be failsafe
#[cfg(target_os = "linux")]
pub type RecommendedWatcher = INotifyWatcher;
/// The recommended `Watcher` implementation for the current platform
///
/// Please use [recommended_watcher_fallback] if you want to be failsafe
#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
pub type RecommendedWatcher = FsEventWatcher;
/// The recommended `Watcher` implementation for the current platform
///
/// Please use [recommended_watcher_fallback] if you want to be failsafe
#[cfg(target_os = "windows")]
pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
/// The recommended `Watcher` implementation for the current platform
///
/// Please use [recommended_watcher_fallback] if you want to be failsafe
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
Expand All @@ -357,6 +369,8 @@ pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
))]
pub type RecommendedWatcher = KqueueWatcher;
/// The recommended `Watcher` implementation for the current platform
///
/// Please use [recommended_watcher_fallback] if you want to be failsafe
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
Expand All @@ -370,16 +384,108 @@ pub type RecommendedWatcher = PollWatcher;

/// Convenience method for creating the `RecommendedWatcher` for the current platform in
/// _immediate_ mode.
///
/// Please note that unlike [recommended_watcher_fallback] this does not guard against unavailable APIs.
///
/// It is not more than calling `RecommendedWatcher::new`
///
/// See [`Watcher::new_immediate`](trait.Watcher.html#tymethod.new_immediate).
/// See [`Watcher::new`](trait.Watcher.html#tymethod.new).
#[deprecated = "See recommended_watcher_fallback, does not guard against unavailable APIs"]
pub fn recommended_watcher<F>(event_handler: F) -> Result<RecommendedWatcher>
where
F: EventHandler,
{
// All recommended watchers currently implement `new`, so just call that.
// TODO: v6 implement fallback, using recommended_watcher_fallback
RecommendedWatcher::new(event_handler, Config::default())
}

/// Method for creating the `RecommendedWatcher` for the current platform or falling back.
///
/// Unlike [recommended_watcher] and [RecommendedWatcher] this falls back if the recommended API is not available.
///
/// Example use case is [#423](https://github.com/notify-rs/notify/issues/423) where Docker with QEMU on M1 chips does not expose the inotify OS API.
///
/// See also [`Watcher::new`](trait.Watcher.html#tymethod.new).
pub fn recommended_watcher_fallback<F>(event_handler: F, config: Config) -> Result<WatcherFallback>
where
F: EventHandler + Copy,
{
// All recommended watchers currently implement `new`, so just call that.
match RecommendedWatcher::new(event_handler, config.clone()) {
Ok(v) => Ok(WatcherFallback::Native(v)),
Err(e) => {
match &e.kind {
#[allow(unused)]
ErrorKind::Io(io_error) => {
#[cfg(target_os = "linux")]
if io_error.raw_os_error() == Some(38) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a documented meaning to 38 or indeed other codes somewhere? I suspect we should fallback in more cases but I have no idea how to find out which ones should be an error and which should fallback.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, here is the manpage though I prefer this list
Problem is that this does not tell us which of these could be emitted by our specific syscall.

return Ok(WatcherFallback::Fallback(PollWatcher::new(event_handler, config)?));
}
},
_ => (),
}
return Err(e);
},
}
}

/// Wrapper for a fallback initialized watcher.
///
/// Can be used like a Watcher.
///
/// See [`recommended_watcher_fallback`](recommended_watcher_fallback)
#[derive(Debug)]
pub enum WatcherFallback {
/// Native watcher, no error occured
Native(RecommendedWatcher),
/// Fallback watcher, known platform issue occured
///
/// For example the Docker with Linux on MacOS M1 bug
Fallback(PollWatcher)
}

impl WatcherFallback {
/// Returns the watcher inside boxed
pub fn take_boxed(self) -> Box<dyn Watcher>{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I would prefer that this implemented watch. Is that very verbose/difficult?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a very good idea! and it would make the enum more worthwhile (because otherwise I don't actually see much of a value over directly boxing it, except for claiming the allocation-free throne, while not being useful at all)

Copy link
Member Author

@0xpr03 0xpr03 Sep 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I tried this.. And it gets awkward.

  • We can't implement Watcher::new due to missing Copy/Clone on event_handler.
  • We can't implement a real kind(), as we don't have access to self. This kind of API is specifically not designed for runtime-selected watchers it seems. (V6 incoming.. ) We could still implement the functions without the trait, or leave them unimplemented!() for the new and the additional watcher-kind as in my new code.

Edit: But we can implement Deref and DerefMut, allowing direct Watcher::* access, but also kind(&self), which isn't the Trait, but at least gives the functionality.

match self {
WatcherFallback::Native(v) => Box::new(v),
WatcherFallback::Fallback(v) => Box::new(v),
}
}

/// Returns the [WatcherKind] used
pub fn kind(&self) -> WatcherKind {
match self {
// we can assume these kinds due to the way we initialize it
WatcherFallback::Native(_) => {
<RecommendedWatcher as Watcher>::kind()
},
WatcherFallback::Fallback(_) => WatcherKind::PollWatcher,
}
}
}

impl std::ops::Deref for WatcherFallback {
type Target = dyn Watcher;

fn deref(&self) -> &Self::Target {
match self {
WatcherFallback::Native(v) => v,
WatcherFallback::Fallback(v) => v,
}
}
}

impl std::ops::DerefMut for WatcherFallback {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
WatcherFallback::Native(v) => v,
WatcherFallback::Fallback(v) => v,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion notify/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ impl ReadDirectoryChangesWatcher {
}

impl Watcher for ReadDirectoryChangesWatcher {
fn new<F: EventHandler>(event_handler: F, config: Config) -> Result<Self> {
fn new<F: EventHandler>(event_handler: F, _config: Config) -> Result<Self> {
// create dummy channel for meta event
// TODO: determine the original purpose of this - can we remove it?
let (meta_tx, _) = unbounded();
Expand Down