From 8e5507b9f2e40834d5dfa1d321d84c7a72f94dd4 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Fri, 6 Nov 2026 21:59:03 +0800 Subject: [PATCH] fix(vfs): hint at inotify limits when the file watcher hits them (#793) On Linux a watcher creation can fail with a bare OS error when the kernel's inotify limits are exhausted: one instance per watcher (EMFILE, os error 24) or one watch per directory (ENOSPC, mapped by notify to MaxFilesWatch). Name the sysctl knob to raise in the error message instead of leaving users to search engines (issue #788). --- crates/ide-vfs/src/watch.rs | 51 +++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/ide-vfs/src/watch.rs b/crates/ide-vfs/src/watch.rs index 47babf368..d00d8778a 100644 --- a/crates/ide-vfs/src/watch.rs +++ b/crates/ide-vfs/src/watch.rs @@ -13,7 +13,7 @@ use crate::FsBatch; use crate::rules::{ExcludeMatcher, GitignoreStack}; use ide_core::{IdeError, IdeEvent, IdeResult, SharedSink}; use notify::event::{ModifyKind, RenameMode}; -use notify::{Event, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, WatcherKind}; +use notify::{ErrorKind, Event, EventKind, PollWatcher, RecommendedWatcher, RecursiveMode, WatcherKind}; use parking_lot::{Condvar, Mutex, RwLock}; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::path::{Component, Path, PathBuf}; @@ -179,7 +179,7 @@ impl Watcher { }, config, ) - .map_err(|e| io_error(format!("cannot start file watcher: {e}")))?; + .map_err(|e| watcher_error("cannot start file watcher", e))?; *state.watcher.lock() = Some(Box::new(watcher)); let deferred = if mode == WatchMode::PerDirectory { state.watch_tree_bounded(root, SYNC_WATCH_BUDGET) @@ -227,6 +227,29 @@ fn is_dir_touch(event: ¬ify::Event) -> bool { matches!(event.kind, EventKind::Modify(_)) && event.paths.iter().all(|p| p.is_dir()) } +const MAX_WATCHES_HINT: &str = "the inotify watch limit is exhausted (fs.inotify.max_user_watches); raise it, e.g. \ + `sudo sysctl -w fs.inotify.max_user_watches=524288`"; + +/// `io_error` with a fix hint when Linux's inotify limits are exhausted: one +/// instance per watcher (`EMFILE`, os error 24) or one watch per directory +/// (`ENOSPC`, mapped by notify to [`ErrorKind::MaxFilesWatch`]). A bare OS +/// error sends users off to search engines otherwise (issue #788). +fn watcher_error(what: &str, error: notify::Error) -> IdeError { + let hint = match error.kind { + ErrorKind::MaxFilesWatch => Some(MAX_WATCHES_HINT), + ErrorKind::Io(ref e) if cfg!(target_os = "linux") && e.raw_os_error() == Some(24) => Some( + "the inotify instance limit is exhausted (fs.inotify.max_user_instances); raise it, e.g. \ + `sudo sysctl -w fs.inotify.max_user_instances=1024`", + ), + ErrorKind::Io(ref e) if cfg!(target_os = "linux") && e.raw_os_error() == Some(28) => Some(MAX_WATCHES_HINT), + _ => None, + }; + match hint { + Some(hint) => io_error(format!("{what}: {error}; {hint}")), + None => io_error(format!("{what}: {error}")), + } +} + /// Shared with the notify callback (weakly) and the debounce thread. struct DirState { filter: EventFilter, @@ -1273,4 +1296,28 @@ mod tests { std::fs::write(root.join("src/new/inner/h.rs"), "x").unwrap(); assert!(wait_for(&batches, |b| all_paths(b).contains(&root.join("src/new/inner/h.rs")))); } + + #[test] + fn ordinary_watcher_errors_keep_the_bare_message() { + let other = notify::Error::io(std::io::Error::other("no space left")); + assert_eq!( + watcher_error("cannot start file watcher", other).message, + "cannot start file watcher: no space left" + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn inotify_limits_get_a_sysctl_hint() { + let emfile = notify::Error::io(std::io::Error::from_raw_os_error(24)); + let msg = watcher_error("cannot start file watcher", emfile).message; + assert!(msg.contains("os error 24"), "{msg}"); + assert!(msg.contains("max_user_instances"), "{msg}"); + let enospc = notify::Error::io(std::io::Error::from_raw_os_error(28)); + assert!(watcher_error("cannot watch /proj", enospc).message.contains("max_user_watches")); + let max_files = notify::Error::new(ErrorKind::MaxFilesWatch); + assert!(watcher_error("cannot watch /proj", max_files).message.contains("max_user_watches")); + let other = notify::Error::io(std::io::Error::from_raw_os_error(13)); + assert!(!watcher_error("cannot start file watcher", other).message.contains("inotify")); + } }