1683 lines · 57.7 KB · Rust
RawDownload
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: &notify::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.