use gpui::{BackgroundExecutor, Task};use notify::{Event, EventKind};use parking_lot::Mutex;use std::{ collections::HashMap, fs, ops::DerefMut, path::Path, sync::{Arc, LazyLock, OnceLock}, time::{Duration, Instant},};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 { executor: BackgroundExecutor, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, registrations: Arc<Mutex<HashMap<WatchKey, FsWatcherRegistration>>>, pending_registrations: Arc<Mutex<HashMap<Arc<std::path::Path>, Task<()>>>>,} #[derive(Clone, Copy)]struct FsWatcherRegistration { id: WatcherRegistrationId, mode: WatcherMode,} impl FsWatcher { pub fn new( executor: BackgroundExecutor, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, ) -> Self { Self { executor, tx, pending_path_events, registrations: Default::default(), pending_registrations: Default::default(), } } fn add_existing_path(&self, path: Arc<Path>) -> anyhow::Result<()> { let case_insensitive = case_insensitive_path(&path); let key = WatchKey::for_registration(SanitizedPath::new(&path), case_insensitive); if self.registrations.lock().contains_key(&key) { log::trace!("path to watch is already watched: {path:?}"); return Ok(()); } match register_existing_path( path.clone(), case_insensitive, self.tx.clone(), self.pending_path_events.clone(), )? { Some(registration) => { self.registrations.lock().insert(key, registration); } None => { // Registration was skipped (e.g. the native watch-limit cooldown // is active). Retry in the background rather than silently leaving // the path unwatched forever. log::warn!("watch registration for {path:?} was skipped; retrying in background"); self.add_pending_path(path); } } Ok(()) } fn add_pending_path(&self, path: Arc<Path>) { let mut pending_registrations = self.pending_registrations.lock(); if pending_registrations.contains_key(path.as_ref()) { return; } let task = self.executor.spawn(poll_path_until_created( self.executor.clone(), path.clone(), self.tx.clone(), self.pending_path_events.clone(), self.registrations.clone(), self.pending_registrations.clone(), )); pending_registrations.insert(path, task); }} impl Drop for FsWatcher { fn drop(&mut self) { self.pending_registrations.lock().clear(); let mut registrations = HashMap::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.id); } }} impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("watcher add: {path:?}"); let path: Arc<Path> = path.into(); if path_covered_by_recursive_registration( &self.registrations.lock(), SanitizedPath::new(&path), ) { log::trace!("path to watch is covered by an existing registration: {path:?}"); return Ok(()); } if self .pending_registrations .lock() .contains_key(path.as_ref()) { log::trace!("path to watch is already pending: {path:?}"); return Ok(()); } if fs::symlink_metadata(path.as_ref()).is_err() { self.add_pending_path(path); return Ok(()); } self.add_existing_path(path) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("remove watched path: {path:?}"); self.pending_registrations.lock().remove(path); let sanitized = SanitizedPath::new(path); let registration = { let mut registrations = self.registrations.lock(); registrations .remove(&WatchKey::exact(sanitized)) .or_else(|| registrations.remove(&WatchKey::folded(sanitized))) }; if let Some(registration) = registration { global_watcher().remove(registration.id); } Ok(()) }} /// Whether a recursive registration on a strict ancestor of `path` already covers/// it. Both key spellings are probed so a folded registration still matches; only/// poll watches and native macOS/Windows watches are recursive.fn path_covered_by_recursive_registration( registrations: &HashMap<WatchKey, FsWatcherRegistration>, path: &SanitizedPath,) -> bool { path.as_path().ancestors().skip(1).any(|ancestor| { let ancestor = SanitizedPath::unchecked_new(ancestor); [WatchKey::exact(ancestor), WatchKey::folded(ancestor)] .iter() .any(|key| { registrations.get(key).is_some_and(|registration| { registration.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")) }) }) })} /// 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 typepub 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")] { return detect_requires_poll_watcher_linux(path); } #[cfg(not(target_os = "linux"))] { let _ = path; false }} fn register_existing_path( path: Arc<Path>, case_insensitive: bool, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>,) -> anyhow::Result<Option<FsWatcherRegistration>> { let mode = if requires_poll_watcher(path.as_ref()) { log::info!( "Using poll watcher ({}ms interval) for {}", poll_interval().as_millis(), path.display() ); telemetry::event!("fs_watcher_poll", path = path.display().to_string()); WatcherMode::Poll } else { WatcherMode::Native }; let root_path = SanitizedPath::new_arc(path.as_ref()); let path_for_callback = path.clone(); let Some(registration_id) = global_watcher().add( path, mode, case_insensitive, move |event: ¬ify::Event| { log::trace!("watcher received event: {event:?}"); push_notify_event( &tx, &pending_path_events, &root_path, case_insensitive, path_for_callback.as_ref(), event, ); }, )? else { return Ok(None); }; Ok(Some(FsWatcherRegistration { id: registration_id, mode, }))} #[cfg(target_os = "linux")]fn detect_requires_poll_watcher_linux(path: &Path) -> bool { use std::ffi::CString; use std::os::unix::ffi::OsStrExt; 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; } const V9FS_MAGIC: u64 = 0x0102_1997; const NFS_SUPER_MAGIC: u64 = 0x0000_6969; const CIFS_MAGIC: u64 = 0xFF53_4D42; const SMB_SUPER_MAGIC: u64 = 0x0000_517B; const SMB2_MAGIC: u64 = 0xFE53_4D42; const FUSE_SUPER_MAGIC: u64 = 0x6573_5546; let fs_type = (stat.f_type as u64) & 0xFFFF_FFFF; if fs_type == FUSE_SUPER_MAGIC && is_virtiofs(path) { return false; } 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; } 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_virtiofs(path: &Path) -> bool { let Ok(mountinfo) = std::fs::read_to_string("/proc/self/mountinfo") else { return false; }; let mut best_mount = None; for line in mountinfo.lines() { let fields = line.split(' ').collect::<Vec<_>>(); let Some(separator) = fields.iter().position(|field| *field == "-") else { continue; }; let (Some(mount_point), Some(fs_type)) = (fields.get(4), fields.get(separator + 1)) else { continue; }; let mount_point = mount_point .replace("\\040", " ") .replace("\\011", "\t") .replace("\\012", "\n") .replace("\\134", "\\"); if path.starts_with(&mount_point) && best_mount.is_none_or(|(length, _)| mount_point.len() > length) { best_mount = Some((mount_point.len(), *fs_type)); } } best_mount.is_some_and(|(_, fs_type)| fs_type == "virtiofs" || fs_type == "fuse.virtiofs")} #[cfg(target_os = "linux")]fn is_wsl_drvfs_path(path: &Path) -> bool { if std::env::var_os("WSL_DISTRO_NAME").is_none() { if let Ok(version) = std::fs::read_to_string("/proc/version") { let version = version.to_lowercase(); if !version.contains("microsoft") && !version.contains("wsl") { return false; } } else { return false; } } let Some(path) = path.to_str() else { return false; }; if !path.starts_with("/mnt/") || path.len() < 6 { return false; } let after_mnt = &path[5..]; after_mnt.starts_with(|c: char| c.is_ascii_alphabetic()) && (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/')} /// Whether the volume backing `path` does case-insensitive name lookups, used to/// pick exact vs. folded matching.#[cfg(target_os = "macos")]fn case_insensitive_path(path: &Path) -> bool { use std::os::unix::ffi::OsStrExt as _; // `pathconf(_PC_CASE_SENSITIVE)` returns 1 (sensitive), 0 (insensitive), or -1 // on error; default errors to insensitive (the APFS/HFS+ default). let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else { return true; }; // SAFETY: We just initialized c_path, so it's a valid pointer unsafe { libc::pathconf(c_path.as_ptr(), libc::_PC_CASE_SENSITIVE) == 0 }} #[cfg(target_os = "linux")]fn case_insensitive_path(_path: &Path) -> bool { // use std::os::unix::ffi::OsStrExt as _; // // Only ext4/f2fs casefold (`+F`) dirs are insensitive, reported by `statx` via // // STATX_ATTR_CASEFOLD; any failure (e.g. pre-4.11 ENOSYS) means case-sensitive. // const STATX_ATTR_CASEFOLD: u64 = 0x0000_2000; // let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else { // return false; // }; // let mut buf = std::mem::MaybeUninit::<libc::statx>::zeroed(); // // SAFETY: c_path is still valid, buffer has been zeroed // if unsafe { libc::statx(libc::AT_FDCWD, c_path.as_ptr(), 0, 0, buf.as_mut_ptr()) } != 0 { // return false; // } // // SAFETY: libc statx initialized this buffer, otherwise we would've returned on a error // // in that function call // let buf = unsafe { buf.assume_init() }; // buf.stx_attributes_mask & STATX_ATTR_CASEFOLD != 0 // && buf.stx_attributes & STATX_ATTR_CASEFOLD != 0 false} #[cfg(target_os = "windows")]fn case_insensitive_path(_path: &Path) -> bool { // todo(windows): Windows defaults to case in sensitive, but // they can mark specific directories as case sensitive. Mainly // for WSL use cases true} #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]fn case_insensitive_path(_path: &Path) -> bool { // Other BSDs default to case-sensitive local filesystems. false} /// Whether `path` is `root` or sits beneath it, folding case on case-insensitive/// volumes so a differently-cased spelling still matches.fn path_is_under(path: &SanitizedPath, root: &SanitizedPath, case_insensitive: bool) -> bool { if case_insensitive { let path = path.as_path().to_string_lossy().to_lowercase(); let root = root.as_path().to_string_lossy().to_lowercase(); Path::new(&path).starts_with(Path::new(&root)) } else { path.starts_with(root) }} /// Lookup key for a watch path, shared by add, remove, and dispatch so they all/// agree on whether two spellings denote the same directory.////// On case-sensitive volumes the exact (sanitized) path is the key, so genuinely/// distinct directories stay distinct. On case-insensitive volumes the folded/// (lowercased) spelling is the key, so any casing of a directory collides.////// The two variants are distinct map keys, so a case-sensitive registration can/// never be hit by a folded lookup (or vice versa); dispatch can therefore probe/// both forms of an event path without risking a cross-rule false match.////// NOTE: folding only normalizes case. macOS (APFS/HFS+) is also Unicode/// normalization-insensitive (NFC vs NFD); that normalization is intentionally/// centralized here so it can be added in one place later without touching the/// add/remove/dispatch call sites.#[derive(Clone, Debug, PartialEq, Eq, Hash)]enum WatchKey { Exact(Arc<Path>), Folded(Arc<str>),} impl WatchKey { fn exact(path: &SanitizedPath) -> Self { Self::Exact(Arc::from(path.as_path())) } fn folded(path: &SanitizedPath) -> Self { let lossy = path.as_path().to_string_lossy(); // macOS (APFS/HFS+) compares names normalization-insensitively (NFC vs // NFD), and FSEvents can report NFD while a config/LSP supplies NFC, so // normalize before folding case. Windows (NTFS) and Linux are // normalization-sensitive, so there we only fold case. #[cfg(target_os = "macos")] let folded = { use unicode_normalization::UnicodeNormalization as _; lossy.chars().nfc().collect::<String>().to_lowercase() }; #[cfg(not(target_os = "macos"))] let folded = lossy.to_lowercase(); Self::Folded(folded.into()) } fn for_registration(path: &SanitizedPath, case_insensitive: bool) -> Self { if case_insensitive { Self::folded(path) } else { Self::exact(path) } }} async fn poll_path_until_created( executor: BackgroundExecutor, path: Arc<Path>, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, registrations: Arc<Mutex<HashMap<WatchKey, FsWatcherRegistration>>>, pending_registrations: Arc<Mutex<HashMap<Arc<Path>, Task<()>>>>,) { loop { executor.timer(poll_interval()).await; if !pending_registrations.lock().contains_key(path.as_ref()) { return; } if smol::fs::symlink_metadata(path.as_ref()).await.is_err() { continue; } Lines 1–500 of 1683. Load more
use gpui::{BackgroundExecutor, Task}; use notify::{Event, EventKind}; use parking_lot::Mutex; use std::{ collections::HashMap, fs, ops::DerefMut, path::Path, sync::{Arc, LazyLock, OnceLock}, time::{Duration, Instant}, }; 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 { executor: BackgroundExecutor, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, registrations: Arc<Mutex<HashMap<WatchKey, FsWatcherRegistration>>>, pending_registrations: Arc<Mutex<HashMap<Arc<std::path::Path>, Task<()>>>>, } #[derive(Clone, Copy)] struct FsWatcherRegistration { id: WatcherRegistrationId, mode: WatcherMode, } impl FsWatcher { pub fn new( executor: BackgroundExecutor, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, ) -> Self { Self { executor, tx, pending_path_events, registrations: Default::default(), pending_registrations: Default::default(), } } fn add_existing_path(&self, path: Arc<Path>) -> anyhow::Result<()> { let case_insensitive = case_insensitive_path(&path); let key = WatchKey::for_registration(SanitizedPath::new(&path), case_insensitive); if self.registrations.lock().contains_key(&key) { log::trace!("path to watch is already watched: {path:?}"); return Ok(()); } match register_existing_path( path.clone(), case_insensitive, self.tx.clone(), self.pending_path_events.clone(), )? { Some(registration) => { self.registrations.lock().insert(key, registration); } None => { // Registration was skipped (e.g. the native watch-limit cooldown // is active). Retry in the background rather than silently leaving // the path unwatched forever. log::warn!("watch registration for {path:?} was skipped; retrying in background"); self.add_pending_path(path); } } Ok(()) } fn add_pending_path(&self, path: Arc<Path>) { let mut pending_registrations = self.pending_registrations.lock(); if pending_registrations.contains_key(path.as_ref()) { return; } let task = self.executor.spawn(poll_path_until_created( self.executor.clone(), path.clone(), self.tx.clone(), self.pending_path_events.clone(), self.registrations.clone(), self.pending_registrations.clone(), )); pending_registrations.insert(path, task); } } impl Drop for FsWatcher { fn drop(&mut self) { self.pending_registrations.lock().clear(); let mut registrations = HashMap::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.id); } } } impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("watcher add: {path:?}"); let path: Arc<Path> = path.into(); if path_covered_by_recursive_registration( &self.registrations.lock(), SanitizedPath::new(&path), ) { log::trace!("path to watch is covered by an existing registration: {path:?}"); return Ok(()); } if self .pending_registrations .lock() .contains_key(path.as_ref()) { log::trace!("path to watch is already pending: {path:?}"); return Ok(()); } if fs::symlink_metadata(path.as_ref()).is_err() { self.add_pending_path(path); return Ok(()); } self.add_existing_path(path) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("remove watched path: {path:?}"); self.pending_registrations.lock().remove(path); let sanitized = SanitizedPath::new(path); let registration = { let mut registrations = self.registrations.lock(); registrations .remove(&WatchKey::exact(sanitized)) .or_else(|| registrations.remove(&WatchKey::folded(sanitized))) }; if let Some(registration) = registration { global_watcher().remove(registration.id); } Ok(()) } } /// Whether a recursive registration on a strict ancestor of `path` already covers /// it. Both key spellings are probed so a folded registration still matches; only /// poll watches and native macOS/Windows watches are recursive. fn path_covered_by_recursive_registration( registrations: &HashMap<WatchKey, FsWatcherRegistration>, path: &SanitizedPath, ) -> bool { path.as_path().ancestors().skip(1).any(|ancestor| { let ancestor = SanitizedPath::unchecked_new(ancestor); [WatchKey::exact(ancestor), WatchKey::folded(ancestor)] .iter() .any(|key| { registrations.get(key).is_some_and(|registration| { registration.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")) }) }) }) } /// 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")] { return detect_requires_poll_watcher_linux(path); } #[cfg(not(target_os = "linux"))] { let _ = path; false } } fn register_existing_path( path: Arc<Path>, case_insensitive: bool, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, ) -> anyhow::Result<Option<FsWatcherRegistration>> { let mode = if requires_poll_watcher(path.as_ref()) { log::info!( "Using poll watcher ({}ms interval) for {}", poll_interval().as_millis(), path.display() ); telemetry::event!("fs_watcher_poll", path = path.display().to_string()); WatcherMode::Poll } else { WatcherMode::Native }; let root_path = SanitizedPath::new_arc(path.as_ref()); let path_for_callback = path.clone(); let Some(registration_id) = global_watcher().add( path, mode, case_insensitive, move |event: ¬ify::Event| { log::trace!("watcher received event: {event:?}"); push_notify_event( &tx, &pending_path_events, &root_path, case_insensitive, path_for_callback.as_ref(), event, ); }, )? else { return Ok(None); }; Ok(Some(FsWatcherRegistration { id: registration_id, mode, })) } #[cfg(target_os = "linux")] fn detect_requires_poll_watcher_linux(path: &Path) -> bool { use std::ffi::CString; use std::os::unix::ffi::OsStrExt; 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; } const V9FS_MAGIC: u64 = 0x0102_1997; const NFS_SUPER_MAGIC: u64 = 0x0000_6969; const CIFS_MAGIC: u64 = 0xFF53_4D42; const SMB_SUPER_MAGIC: u64 = 0x0000_517B; const SMB2_MAGIC: u64 = 0xFE53_4D42; const FUSE_SUPER_MAGIC: u64 = 0x6573_5546; let fs_type = (stat.f_type as u64) & 0xFFFF_FFFF; if fs_type == FUSE_SUPER_MAGIC && is_virtiofs(path) { return false; } 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; } 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_virtiofs(path: &Path) -> bool { let Ok(mountinfo) = std::fs::read_to_string("/proc/self/mountinfo") else { return false; }; let mut best_mount = None; for line in mountinfo.lines() { let fields = line.split(' ').collect::<Vec<_>>(); let Some(separator) = fields.iter().position(|field| *field == "-") else { continue; }; let (Some(mount_point), Some(fs_type)) = (fields.get(4), fields.get(separator + 1)) else { continue; }; let mount_point = mount_point .replace("\\040", " ") .replace("\\011", "\t") .replace("\\012", "\n") .replace("\\134", "\\"); if path.starts_with(&mount_point) && best_mount.is_none_or(|(length, _)| mount_point.len() > length) { best_mount = Some((mount_point.len(), *fs_type)); } } best_mount.is_some_and(|(_, fs_type)| fs_type == "virtiofs" || fs_type == "fuse.virtiofs") } #[cfg(target_os = "linux")] fn is_wsl_drvfs_path(path: &Path) -> bool { if std::env::var_os("WSL_DISTRO_NAME").is_none() { if let Ok(version) = std::fs::read_to_string("/proc/version") { let version = version.to_lowercase(); if !version.contains("microsoft") && !version.contains("wsl") { return false; } } else { return false; } } let Some(path) = path.to_str() else { return false; }; if !path.starts_with("/mnt/") || path.len() < 6 { return false; } let after_mnt = &path[5..]; after_mnt.starts_with(|c: char| c.is_ascii_alphabetic()) && (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/') } /// Whether the volume backing `path` does case-insensitive name lookups, used to /// pick exact vs. folded matching. #[cfg(target_os = "macos")] fn case_insensitive_path(path: &Path) -> bool { use std::os::unix::ffi::OsStrExt as _; // `pathconf(_PC_CASE_SENSITIVE)` returns 1 (sensitive), 0 (insensitive), or -1 // on error; default errors to insensitive (the APFS/HFS+ default). let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else { return true; }; // SAFETY: We just initialized c_path, so it's a valid pointer unsafe { libc::pathconf(c_path.as_ptr(), libc::_PC_CASE_SENSITIVE) == 0 } } #[cfg(target_os = "linux")] fn case_insensitive_path(_path: &Path) -> bool { // use std::os::unix::ffi::OsStrExt as _; // // Only ext4/f2fs casefold (`+F`) dirs are insensitive, reported by `statx` via // // STATX_ATTR_CASEFOLD; any failure (e.g. pre-4.11 ENOSYS) means case-sensitive. // const STATX_ATTR_CASEFOLD: u64 = 0x0000_2000; // let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else { // return false; // }; // let mut buf = std::mem::MaybeUninit::<libc::statx>::zeroed(); // // SAFETY: c_path is still valid, buffer has been zeroed // if unsafe { libc::statx(libc::AT_FDCWD, c_path.as_ptr(), 0, 0, buf.as_mut_ptr()) } != 0 { // return false; // } // // SAFETY: libc statx initialized this buffer, otherwise we would've returned on a error // // in that function call // let buf = unsafe { buf.assume_init() }; // buf.stx_attributes_mask & STATX_ATTR_CASEFOLD != 0 // && buf.stx_attributes & STATX_ATTR_CASEFOLD != 0 false } #[cfg(target_os = "windows")] fn case_insensitive_path(_path: &Path) -> bool { // todo(windows): Windows defaults to case in sensitive, but // they can mark specific directories as case sensitive. Mainly // for WSL use cases true } #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] fn case_insensitive_path(_path: &Path) -> bool { // Other BSDs default to case-sensitive local filesystems. false } /// Whether `path` is `root` or sits beneath it, folding case on case-insensitive /// volumes so a differently-cased spelling still matches. fn path_is_under(path: &SanitizedPath, root: &SanitizedPath, case_insensitive: bool) -> bool { if case_insensitive { let path = path.as_path().to_string_lossy().to_lowercase(); let root = root.as_path().to_string_lossy().to_lowercase(); Path::new(&path).starts_with(Path::new(&root)) } else { path.starts_with(root) } } /// Lookup key for a watch path, shared by add, remove, and dispatch so they all /// agree on whether two spellings denote the same directory. /// /// On case-sensitive volumes the exact (sanitized) path is the key, so genuinely /// distinct directories stay distinct. On case-insensitive volumes the folded /// (lowercased) spelling is the key, so any casing of a directory collides. /// /// The two variants are distinct map keys, so a case-sensitive registration can /// never be hit by a folded lookup (or vice versa); dispatch can therefore probe /// both forms of an event path without risking a cross-rule false match. /// /// NOTE: folding only normalizes case. macOS (APFS/HFS+) is also Unicode /// normalization-insensitive (NFC vs NFD); that normalization is intentionally /// centralized here so it can be added in one place later without touching the /// add/remove/dispatch call sites. #[derive(Clone, Debug, PartialEq, Eq, Hash)] enum WatchKey { Exact(Arc<Path>), Folded(Arc<str>), } impl WatchKey { fn exact(path: &SanitizedPath) -> Self { Self::Exact(Arc::from(path.as_path())) } fn folded(path: &SanitizedPath) -> Self { let lossy = path.as_path().to_string_lossy(); // macOS (APFS/HFS+) compares names normalization-insensitively (NFC vs // NFD), and FSEvents can report NFD while a config/LSP supplies NFC, so // normalize before folding case. Windows (NTFS) and Linux are // normalization-sensitive, so there we only fold case. #[cfg(target_os = "macos")] let folded = { use unicode_normalization::UnicodeNormalization as _; lossy.chars().nfc().collect::<String>().to_lowercase() }; #[cfg(not(target_os = "macos"))] let folded = lossy.to_lowercase(); Self::Folded(folded.into()) } fn for_registration(path: &SanitizedPath, case_insensitive: bool) -> Self { if case_insensitive { Self::folded(path) } else { Self::exact(path) } } } async fn poll_path_until_created( executor: BackgroundExecutor, path: Arc<Path>, tx: async_channel::Sender<()>, pending_path_events: Arc<Mutex<Vec<PathEvent>>>, registrations: Arc<Mutex<HashMap<WatchKey, FsWatcherRegistration>>>, pending_registrations: Arc<Mutex<HashMap<Arc<Path>, Task<()>>>>, ) { loop { executor.timer(poll_interval()).await; if !pending_registrations.lock().contains_key(path.as_ref()) { return; } if smol::fs::symlink_metadata(path.as_ref()).await.is_err() { continue; }