From 68f1d088c197062e76220d68d358babd217a11bd Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 23 Apr 2026 06:40:49 -0500 Subject: [PATCH] fs: Polling backend support (#54481) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #51340 Release Notes: - Linux/WSL: Added polling filesystem watching support in order to support file watching on file systems that do emit events through `inotify`, for example `/mnt/c` in WSL, network filesystems, and FUSE mounts. Polling should be automatically chosen over `inotify` when necessary, however it can be manually chosen by setting the `ZED_FILE_WATCHER_MODE=poll` env var --------- Co-authored-by: Lilith River Co-authored-by: Jakub Konka --- Cargo.lock | 1 + crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 158 +++++++++++- crates/fs/src/fs_watcher.rs | 472 +++++++++++++++++++++++------------- 4 files changed, 459 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f58e23ada3..4ed668f605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6606,6 +6606,7 @@ dependencies = [ "serde", "serde_json", "smol", + "telemetry", "tempfile", "text", "thiserror 2.0.17", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index e7b8dcd4eb..eefe2717f2 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -36,6 +36,7 @@ thiserror.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true +telemetry.workspace = true tempfile.workspace = true text.workspace = true time.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 6694b19e37..6b7e4816a1 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -72,6 +72,133 @@ pub trait Watcher: Send + Sync { fn remove(&self, path: &Path) -> Result<()>; } +/// Detect whether a path requires polling instead of native file watching. +/// +/// Returns `true` for filesystem types where inotify/FSEvents/ReadDirectoryChanges +/// silently fail to deliver events: 9P (WSL drvfs), NFS, CIFS/SMB, FUSE (sshfs), etc. +/// +/// Can be overridden with the `ZED_FILE_WATCHER_MODE` environment variable: +/// - `native` — always use native OS watcher +/// - `poll` — always use polling +/// - `auto` (default) — auto-detect based on filesystem type +pub fn requires_poll_watcher(path: &Path) -> bool { + match std::env::var("ZED_FILE_WATCHER_MODE") + .as_deref() + .unwrap_or("auto") + { + "native" => return false, + "poll" => return true, + _ => {} + } + + #[cfg(target_os = "linux")] + { + let path = effective_watch_path(path); + return detect_requires_poll_watcher_linux(&path); + } + + #[cfg(not(target_os = "linux"))] + { + let _ = path; + false + } +} + +pub fn effective_watch_path(path: &Path) -> PathBuf { + if path.exists() { + return path.to_path_buf(); + } + + for ancestor in path.ancestors() { + if ancestor.exists() { + return ancestor.to_path_buf(); + } + } + + path.to_path_buf() +} + +#[cfg(target_os = "linux")] +fn detect_requires_poll_watcher_linux(path: &Path) -> bool { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + + // Check filesystem type via statfs + let c_path = match CString::new(path.as_os_str().as_bytes()) { + Ok(p) => p, + Err(_) => return false, + }; + + let mut stat: libc::statfs = unsafe { std::mem::zeroed() }; + if unsafe { libc::statfs(c_path.as_ptr(), &mut stat) } != 0 { + return false; + } + + // Filesystem magic numbers where inotify does not deliver events. + // These are defined in linux/magic.h and statfs(2). + const V9FS_MAGIC: i64 = 0x01021997; // Plan 9 / WSL2 interop (drvfs) + const NFS_SUPER_MAGIC: i64 = 0x6969; + const CIFS_MAGIC: i64 = 0xFF534D42u32 as i64; // CIFS/SMB + const SMB_SUPER_MAGIC: i64 = 0x517B; + const SMB2_MAGIC: i64 = 0xFE534D42u32 as i64; + const FUSE_SUPER_MAGIC: i64 = 0x65735546; // FUSE (includes sshfs) + + let fs_type = stat.f_type; + if fs_type == V9FS_MAGIC + || fs_type == NFS_SUPER_MAGIC + || fs_type == CIFS_MAGIC + || fs_type == SMB_SUPER_MAGIC + || fs_type == SMB2_MAGIC + || fs_type == FUSE_SUPER_MAGIC + { + log::info!( + "Detected network/virtual filesystem (type 0x{:x}) at {}, using poll watcher", + fs_type, + path.display() + ); + return true; + } + + // Also check for WSL + /mnt// pattern as a fallback + // in case statfs returns an unexpected type for drvfs + if is_wsl_drvfs_path(path) { + log::info!( + "Detected WSL drvfs mount at {}, using poll watcher", + path.display() + ); + return true; + } + + false +} + +#[cfg(target_os = "linux")] +fn is_wsl_drvfs_path(path: &Path) -> bool { + // Only relevant inside WSL + if std::env::var_os("WSL_DISTRO_NAME").is_none() { + if let Ok(version) = std::fs::read_to_string("/proc/version") { + let v = version.to_lowercase(); + if !v.contains("microsoft") && !v.contains("wsl") { + return false; + } + } else { + return false; + } + } + + // Windows drives are mounted at /mnt/c, /mnt/d, etc. + let path_str = match path.to_str() { + Some(s) => s, + None => return false, + }; + if !path_str.starts_with("/mnt/") || path_str.len() < 6 { + return false; + } + let after_mnt = &path_str[5..]; + after_mnt.starts_with(|c: char| c.is_ascii_alphabetic()) + && (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/') +} + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum PathEventKind { Removed, @@ -1070,19 +1197,34 @@ impl Fs for RealFs { use util::{ResultExt as _, paths::SanitizedPath}; let executor = self.executor.clone(); + let use_poll = requires_poll_watcher(path); + let watch_path = effective_watch_path(path); + let (tx, rx) = smol::channel::unbounded(); let pending_paths: Arc>> = Default::default(); - let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); - // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. - if let Err(e) = watcher.add(path) - && let Some(parent) = path.parent() - && let Err(parent_e) = watcher.add(parent) - { + let mode = if use_poll { + log::info!( + "Using poll watcher ({}ms interval) for {}", + fs_watcher::poll_interval().as_millis(), + path.display() + ); + telemetry::event!("fs_watcher_poll", path = path.display().to_string()); + fs_watcher::WatcherMode::Poll + } else { + fs_watcher::WatcherMode::Native + }; + let watcher: Arc = Arc::new(fs_watcher::FsWatcher::new( + tx.clone(), + pending_paths.clone(), + mode, + )); + + if let Err(e) = watcher.add(&watch_path) { log::warn!( - "Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}", + "Failed to watch {} using {}:\n{e}", path.display(), - parent.display() + watch_path.display() ); } diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 02a6b08781..909424558b 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -4,27 +4,38 @@ use std::{ collections::{BTreeMap, HashMap}, ops::DerefMut, path::Path, - sync::{Arc, OnceLock}, + sync::{Arc, LazyLock, OnceLock}, + time::Duration, }; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum WatcherMode { + #[default] + Native, + Poll, +} + pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, registrations: Mutex, WatcherRegistrationId>>, + mode: WatcherMode, } impl FsWatcher { pub fn new( tx: smol::channel::Sender<()>, pending_path_events: Arc>>, + mode: WatcherMode, ) -> Self { Self { tx, pending_path_events, registrations: Default::default(), + mode, } } } @@ -37,11 +48,10 @@ impl Drop for FsWatcher { std::mem::swap(old.deref_mut(), &mut registrations); } - let _ = global(|g| { - for (_, registration) in registrations { - g.remove(registration); - } - }); + let global_watcher = global_watcher(); + for (_, registration) in registrations { + global_watcher.remove(registration); + } } } @@ -49,13 +59,10 @@ impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("watcher add: {path:?}"); let tx = self.tx.clone(); - let pending_paths = self.pending_path_events.clone(); + let pending_path_events = self.pending_path_events.clone(); - #[cfg(any(target_os = "windows", target_os = "macos"))] - { - // Return early if an ancestor of this path was already being watched. - // saves a huge amount of memory - if let Some((watched_path, _)) = self + if (self.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos"))) + && let Some((watched_path, _)) = self .registrations .lock() .range::(( @@ -63,86 +70,36 @@ impl Watcher for FsWatcher { std::ops::Bound::Included(path), )) .next_back() - && path.starts_with(watched_path.as_ref()) - { - log::trace!( - "path to watch is covered by existing registration: {path:?}, {watched_path:?}" - ); - return Ok(()); - } - } - #[cfg(target_os = "linux")] + && path.starts_with(watched_path.as_ref()) { - if self.registrations.lock().contains_key(path) { - log::trace!("path to watch is already watched: {path:?}"); - return Ok(()); - } + log::trace!( + "path to watch is covered by existing registration: {path:?}, {watched_path:?}" + ); + return Ok(()); + } + + if self.registrations.lock().contains_key(path) { + log::trace!("path to watch is already watched: {path:?}"); + return Ok(()); } let root_path = SanitizedPath::new_arc(path); let path: Arc = path.into(); - #[cfg(any(target_os = "windows", target_os = "macos"))] - let mode = notify::RecursiveMode::Recursive; - #[cfg(target_os = "linux")] - let mode = notify::RecursiveMode::NonRecursive; - let registration_path = path.clone(); - let registration_id = global({ - let watch_path = path.clone(); - let callback_path = path; - |g| { - g.add(watch_path, mode, move |event: ¬ify::Event| { + let registration_id = global_watcher().add( + path.clone(), + self.mode, + move |result: Result<¬ify::Event, ¬ify::Error>| match result { + Ok(event) => { log::trace!("watcher received event: {event:?}"); - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::new(event_path); - event_path.starts_with(&root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, - }) - }) - .collect::>(); - - let is_rescan_event = event.need_rescan(); - if is_rescan_event { - log::warn!( - "filesystem watcher lost sync for {callback_path:?}; scheduling rescan" - ); - // we only keep the first event per path below, this ensures it will be the rescan event - // we'll remove any existing pending events for the same reason once we have the lock below - path_events.retain(|p| &p.path != callback_path.as_ref()); - path_events.push(PathEvent { - path: callback_path.to_path_buf(), - kind: Some(PathEventKind::Rescan), - }); - } - - if !path_events.is_empty() { - path_events.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); - } - coalesce_pending_rescans(&mut pending_paths, &mut path_events); - util::extend_sorted( - &mut *pending_paths, - path_events, - usize::MAX, - |a, b| a.path.cmp(&b.path), - ); - } - }) - } - })??; + push_notify_event(&tx, &pending_path_events, &root_path, path.as_ref(), event); + } + Err(error) => { + push_notify_error(&tx, &pending_path_events, path.as_ref(), error); + } + }, + )?; self.registrations .lock() @@ -157,10 +114,85 @@ impl Watcher for FsWatcher { return Ok(()); }; - global(|w| w.remove(registration)) + global_watcher().remove(registration); + Ok(()) } } +fn enqueue_path_events( + tx: &smol::channel::Sender<()>, + pending_path_events: &Arc>>, + mut path_events: Vec, +) { + if path_events.is_empty() { + return; + } + + path_events.sort(); + let mut pending_paths = pending_path_events.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); + } + coalesce_pending_rescans(&mut pending_paths, &mut path_events); + util::extend_sorted(&mut *pending_paths, path_events, usize::MAX, |a, b| { + a.path.cmp(&b.path) + }); +} + +fn push_notify_event( + tx: &smol::channel::Sender<()>, + pending_path_events: &Arc>>, + root_path: &SanitizedPath, + watched_root: &Path, + event: ¬ify::Event, +) { + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::new(event_path); + event_path.starts_with(root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, + }) + }) + .collect::>(); + + if event.need_rescan() { + log::warn!("filesystem watcher lost sync for {watched_root:?}; scheduling rescan"); + path_events.retain(|path_event| path_event.path != watched_root); + path_events.push(PathEvent { + path: watched_root.to_path_buf(), + kind: Some(PathEventKind::Rescan), + }); + } + + enqueue_path_events(tx, pending_path_events, path_events); +} + +fn push_notify_error( + tx: &smol::channel::Sender<()>, + pending_path_events: &Arc>>, + watched_root: &Path, + error: ¬ify::Error, +) { + log::warn!("watcher error for {watched_root:?}: {error}"); + enqueue_path_events( + tx, + pending_path_events, + vec![PathEvent { + path: watched_root.to_path_buf(), + kind: Some(PathEventKind::Rescan), + }], + ); +} + fn coalesce_pending_rescans(pending_paths: &mut Vec, path_events: &mut Vec) { if !path_events .iter() @@ -215,29 +247,34 @@ fn is_covered_rescan(kind: Option, path: &Path, ancestor: &Path) pub struct WatcherRegistrationId(u32); struct WatcherRegistrationState { - callback: Arc, + callback: Arc Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync>, path: Arc, + mode: WatcherMode, } struct WatcherState { watchers: HashMap, - path_registrations: HashMap, u32>, + native_path_registrations: HashMap, u32>, + poll_path_registrations: HashMap, u32>, last_registration: WatcherRegistrationId, } +impl WatcherState { + fn path_registrations(&mut self, mode: WatcherMode) -> &mut HashMap, u32> { + match mode { + WatcherMode::Native => &mut self.native_path_registrations, + WatcherMode::Poll => &mut self.poll_path_registrations, + } + } +} + pub struct GlobalWatcher { state: Mutex, - // DANGER: never keep the state lock while holding the watcher lock - // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. - #[cfg(target_os = "linux")] - watcher: Mutex, - #[cfg(target_os = "freebsd")] - watcher: Mutex, - #[cfg(target_os = "windows")] - watcher: Mutex, - #[cfg(target_os = "macos")] - watcher: Mutex, + // DANGER: never keep state lock while holding watcher lock + // two mutexes because calling watcher.add triggers watcher.event, which needs watchers. + native_watcher: Mutex>, + poll_watcher: Mutex>, } impl GlobalWatcher { @@ -245,27 +282,17 @@ impl GlobalWatcher { fn add( &self, path: Arc, - mode: notify::RecursiveMode, - cb: impl Fn(¬ify::Event) + Send + Sync + 'static, + mode: WatcherMode, + cb: impl for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync + 'static, ) -> anyhow::Result { - use notify::Watcher; - let mut state = self.state.lock(); + let registrations_for_mode = state.path_registrations(mode); + let path_already_covered = + path_already_covered(path.as_ref(), registrations_for_mode, mode); - // Check if this path is already covered by an existing watched ancestor path. - // On macOS and Windows, watching is recursive, so we don't need to watch - // child paths if an ancestor is already being watched. - #[cfg(any(target_os = "windows", target_os = "macos"))] - let path_already_covered = state.path_registrations.keys().any(|existing| { - path.starts_with(existing.as_ref()) && path.as_ref() != existing.as_ref() - }); - - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - let path_already_covered = false; - - if !path_already_covered && !state.path_registrations.contains_key(&path) { + if !path_already_covered && !registrations_for_mode.contains_key(&path) { drop(state); - self.watcher.lock().watch(&path, mode)?; + self.watch(&path, mode)?; state = self.state.lock(); } @@ -275,38 +302,190 @@ impl GlobalWatcher { let registration_state = WatcherRegistrationState { callback: Arc::new(cb), path: path.clone(), + mode, }; state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path).or_insert(0) += 1; + *state.path_registrations(mode).entry(path).or_insert(0) += 1; Ok(id) } pub fn remove(&self, id: WatcherRegistrationId) { - use notify::Watcher; let mut state = self.state.lock(); let Some(registration_state) = state.watchers.remove(&id) else { return; }; - let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { + let path_registrations = state.path_registrations(registration_state.mode); + let Some(count) = path_registrations.get_mut(®istration_state.path) else { return; }; *count -= 1; if *count == 0 { - state.path_registrations.remove(®istration_state.path); + path_registrations.remove(®istration_state.path); + let path_still_covered = path_already_covered( + registration_state.path.as_ref(), + path_registrations, + registration_state.mode, + ); - drop(state); - self.watcher - .lock() - .unwatch(®istration_state.path) - .log_err(); + if !path_still_covered { + drop(state); + self.unwatch(®istration_state.path, registration_state.mode) + .log_err(); + } } } + + fn watch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> { + use notify::Watcher; + + match mode { + WatcherMode::Native => { + self.ensure_native_watcher()?; + self.native_watcher + .lock() + .as_mut() + .expect("native watcher initialized") + .watch( + path, + if cfg!(any(target_os = "windows", target_os = "macos")) { + notify::RecursiveMode::Recursive + } else { + notify::RecursiveMode::NonRecursive + }, + )?; + } + WatcherMode::Poll => { + self.ensure_poll_watcher()?; + self.poll_watcher + .lock() + .as_mut() + .expect("poll watcher initialized") + .watch(path, notify::RecursiveMode::Recursive)?; + } + } + + Ok(()) + } + + fn unwatch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> { + use notify::Watcher; + + match mode { + WatcherMode::Native => { + if let Some(watcher) = self.native_watcher.lock().as_mut() { + watcher.unwatch(path)?; + } + } + WatcherMode::Poll => { + if let Some(watcher) = self.poll_watcher.lock().as_mut() { + watcher.unwatch(path)?; + } + } + } + + Ok(()) + } + + fn ensure_native_watcher(&self) -> anyhow::Result<()> { + if self.native_watcher.lock().is_some() { + return Ok(()); + } + + let watcher = notify::recommended_watcher(handle_native_event)?; + *self.native_watcher.lock() = Some(watcher); + Ok(()) + } + + fn ensure_poll_watcher(&self) -> anyhow::Result<()> { + if self.poll_watcher.lock().is_some() { + return Ok(()); + } + + let config = notify::Config::default().with_poll_interval(*POLL_INTERVAL); + let watcher = notify::PollWatcher::new(handle_poll_event, config)?; + *self.poll_watcher.lock() = Some(watcher); + Ok(()) + } } -static FS_WATCHER_INSTANCE: OnceLock> = - OnceLock::new(); +fn path_already_covered( + path: &Path, + path_registrations: &HashMap, u32>, + mode: WatcherMode, +) -> bool { + (mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos"))) + && path_registrations + .keys() + .any(|existing| path.starts_with(existing.as_ref()) && path != existing.as_ref()) +} + +static POLL_INTERVAL: LazyLock = LazyLock::new(|| { + let poll_ms: u64 = std::env::var("ZED_FILE_WATCHER_POLL_MS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(2000) + .clamp(500, 30000); + Duration::from_millis(poll_ms) +}); + +pub fn poll_interval() -> Duration { + *POLL_INTERVAL +} + +static FS_WATCHER_INSTANCE: OnceLock = OnceLock::new(); + +fn global_watcher() -> &'static GlobalWatcher { + FS_WATCHER_INSTANCE.get_or_init(|| GlobalWatcher { + state: Mutex::new(WatcherState { + watchers: Default::default(), + native_path_registrations: Default::default(), + poll_path_registrations: Default::default(), + last_registration: Default::default(), + }), + native_watcher: Mutex::new(None), + poll_watcher: Mutex::new(None), + }) +} + +fn handle_native_event(event: Result) { + handle_event(WatcherMode::Native, event); +} + +fn handle_poll_event(event: Result) { + handle_event(WatcherMode::Poll, event); +} + +fn handle_event(mode: WatcherMode, event: Result) { + log::trace!("global handle event for {mode:?}: {event:?}"); + + let callbacks = { + let state = global_watcher().state.lock(); + state + .watchers + .values() + .filter(|registration| registration.mode == mode) + .map(|registration| registration.callback.clone()) + .collect::>() + }; + + match event { + Ok(event) => { + if matches!(event.kind, EventKind::Access(_)) { + return; + } + for callback in callbacks { + callback(Ok(&event)); + } + } + Err(error) => { + for callback in callbacks { + callback(Err(&error)); + } + } + } +} #[cfg(test)] mod tests { @@ -399,45 +578,8 @@ mod tests { } } -fn handle_event(event: Result) { - log::trace!("global handle event: {event:?}"); - // Filter out access events, which could lead to a weird bug on Linux after upgrading notify - // https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832 - let Some(event) = event - .log_err() - .filter(|event| !matches!(event.kind, EventKind::Access(_))) - else { - return; - }; - global::<()>(move |watcher| { - let callbacks = { - let state = watcher.state.lock(); - state - .watchers - .values() - .map(|r| r.callback.clone()) - .collect::>() - }; - for callback in callbacks { - callback(&event); - } - }) - .log_err(); -} - pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { - let result = FS_WATCHER_INSTANCE.get_or_init(|| { - notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { - state: Mutex::new(WatcherState { - watchers: Default::default(), - path_registrations: Default::default(), - last_registration: Default::default(), - }), - watcher: Mutex::new(file_watcher), - }) - }); - match result { - Ok(g) => Ok(f(g)), - Err(e) => Err(anyhow::anyhow!("{e}")), - } + let global_watcher = global_watcher(); + global_watcher.ensure_native_watcher()?; + Ok(f(global_watcher)) }