use notify::{Event, EventKind}; use parking_lot::Mutex; use std::{ collections::{BTreeMap, HashMap}, ops::DerefMut, path::Path, 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: async_channel::Sender<()>, pending_path_events: Arc>>, registrations: Mutex, WatcherRegistrationId>>, mode: WatcherMode, } impl FsWatcher { pub fn new( tx: async_channel::Sender<()>, pending_path_events: Arc>>, mode: WatcherMode, ) -> Self { Self { tx, pending_path_events, registrations: Default::default(), mode, } } } impl Drop for FsWatcher { fn drop(&mut self) { let mut registrations = BTreeMap::new(); { let old = &mut self.registrations.lock(); std::mem::swap(old.deref_mut(), &mut registrations); } let global_watcher = global_watcher(); for (_, registration) in registrations { global_watcher.remove(registration); } } } 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_path_events = self.pending_path_events.clone(); if (self.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos"))) && let Some((watched_path, _)) = self .registrations .lock() .range::(( std::ops::Bound::Unbounded, 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(()); } 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(); let registration_path = path.clone(); let registration_id = global_watcher().add(path.clone(), self.mode, move |event: ¬ify::Event| { log::trace!("watcher received event: {event:?}"); push_notify_event(&tx, &pending_path_events, &root_path, path.as_ref(), event); })?; self.registrations .lock() .insert(registration_path, registration_id); Ok(()) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("remove watched path: {path:?}"); let Some(registration) = self.registrations.lock().remove(path) else { return Ok(()); }; 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 coalesce_pending_rescans(pending_paths: &mut Vec, path_events: &mut Vec) { if !path_events .iter() .any(|event| event.kind == Some(PathEventKind::Rescan)) { return; } let mut new_rescan_paths: Vec = path_events .iter() .filter(|e| e.kind == Some(PathEventKind::Rescan)) .map(|e| e.path.clone()) .collect(); new_rescan_paths.sort_unstable(); let mut deduped_rescans: Vec = Vec::with_capacity(new_rescan_paths.len()); for path in new_rescan_paths { if deduped_rescans .iter() .any(|ancestor| path != *ancestor && path.starts_with(ancestor)) { continue; } deduped_rescans.push(path); } deduped_rescans.retain(|new_path| { !pending_paths .iter() .any(|pending| is_covered_rescan(pending.kind, new_path, &pending.path)) }); if !deduped_rescans.is_empty() { pending_paths.retain(|pending| { !deduped_rescans.iter().any(|rescan_path| { pending.path == *rescan_path || is_covered_rescan(pending.kind, &pending.path, rescan_path) }) }); } path_events.retain(|event| { event.kind != Some(PathEventKind::Rescan) || deduped_rescans.contains(&event.path) }); } fn is_covered_rescan(kind: Option, path: &Path, ancestor: &Path) -> bool { kind == Some(PathEventKind::Rescan) && path != ancestor && path.starts_with(ancestor) } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct WatcherRegistrationId(u32); struct WatcherRegistrationState { callback: Arc, path: Arc, mode: WatcherMode, } struct WatcherState { watchers: HashMap, 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 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 { #[must_use] fn add( &self, path: Arc, mode: WatcherMode, cb: impl Fn(¬ify::Event) + Send + Sync + 'static, ) -> anyhow::Result { 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); if !path_already_covered && !registrations_for_mode.contains_key(&path) { drop(state); self.watch(&path, mode)?; state = self.state.lock(); } let id = state.last_registration; state.last_registration = WatcherRegistrationId(id.0 + 1); let registration_state = WatcherRegistrationState { callback: Arc::new(cb), path: path.clone(), mode, }; state.watchers.insert(id, registration_state); *state.path_registrations(mode).entry(path).or_insert(0) += 1; Ok(id) } pub fn remove(&self, id: WatcherRegistrationId) { let mut state = self.state.lock(); let Some(registration_state) = state.watchers.remove(&id) else { return; }; 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 { path_registrations.remove(®istration_state.path); let path_still_covered = path_already_covered( registration_state.path.as_ref(), path_registrations, registration_state.mode, ); 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(()) } } 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) { if matches!( event, Ok(Event { kind: EventKind::Access(_), .. }) ) { return; } 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) => { for callback in callbacks { callback(&event); } } Err(error) => { log::warn!("watcher error for {mode:?}: {error}"); } } } #[cfg(test)] mod tests { use super::*; use std::path::PathBuf; fn rescan(path: &str) -> PathEvent { PathEvent { path: PathBuf::from(path), kind: Some(PathEventKind::Rescan), } } fn changed(path: &str) -> PathEvent { PathEvent { path: PathBuf::from(path), kind: Some(PathEventKind::Changed), } } struct TestCase { name: &'static str, pending_paths: Vec, path_events: Vec, expected_pending_paths: Vec, expected_path_events: Vec, } #[test] fn test_coalesce_pending_rescans() { let test_cases = [ TestCase { name: "coalesces descendant rescans under pending ancestor", pending_paths: vec![rescan("/root")], path_events: vec![rescan("/root/child"), rescan("/root/child/grandchild")], expected_pending_paths: vec![rescan("/root")], expected_path_events: vec![], }, TestCase { name: "new ancestor rescan replaces pending descendant rescans", pending_paths: vec![ changed("/other"), rescan("/root/child"), rescan("/root/child/grandchild"), ], path_events: vec![rescan("/root")], expected_pending_paths: vec![changed("/other")], expected_path_events: vec![rescan("/root")], }, TestCase { name: "same path rescan replaces pending non-rescan event", pending_paths: vec![changed("/root")], path_events: vec![rescan("/root")], expected_pending_paths: vec![], expected_path_events: vec![rescan("/root")], }, TestCase { name: "unrelated rescans are preserved", pending_paths: vec![rescan("/root-a")], path_events: vec![rescan("/root-b")], expected_pending_paths: vec![rescan("/root-a")], expected_path_events: vec![rescan("/root-b")], }, TestCase { name: "batch ancestor rescan replaces descendant rescan", pending_paths: vec![], path_events: vec![rescan("/root/child"), rescan("/root")], expected_pending_paths: vec![], expected_path_events: vec![rescan("/root")], }, ]; for test_case in test_cases { let mut pending_paths = test_case.pending_paths; let mut path_events = test_case.path_events; coalesce_pending_rescans(&mut pending_paths, &mut path_events); assert_eq!( pending_paths, test_case.expected_pending_paths, "pending_paths mismatch for case: {}", test_case.name ); assert_eq!( path_events, test_case.expected_path_events, "path_events mismatch for case: {}", test_case.name ); } } } pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { let global_watcher = global_watcher(); global_watcher.ensure_native_watcher()?; Ok(f(global_watcher)) }