diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 733d341742..6ee5f05148 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -140,10 +140,7 @@ pub struct RealFs { git_binary_path: Option, } -pub struct RealWatcher { - #[cfg(target_os = "linux")] - fs_watcher: parking_lot::Mutex, -} +pub struct RealWatcher {} impl RealFs { pub fn new( @@ -472,29 +469,29 @@ impl Fs for RealFs { let pending_paths: Arc>> = Default::default(); let root_path = path.to_path_buf(); - let file_watcher = notify::recommended_watcher({ + watcher::global(|g| { let tx = tx.clone(); let pending_paths = pending_paths.clone(); - move |event: Result| { - if let Some(event) = event.log_err() { - let mut paths = event.paths; - paths.retain(|path| path.starts_with(&root_path)); - if !paths.is_empty() { - paths.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); - } - util::extend_sorted(&mut *pending_paths, paths, usize::MAX, PathBuf::cmp); + g.add(move |event: ¬ify::Event| { + let mut paths = event + .paths + .iter() + .filter(|path| path.starts_with(&root_path)) + .cloned() + .collect::>(); + if !paths.is_empty() { + paths.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); } + util::extend_sorted(&mut *pending_paths, paths, usize::MAX, PathBuf::cmp); } - } + }) }) - .expect("Could not start file watcher"); + .log_err(); - let watcher = Arc::new(RealWatcher { - fs_watcher: parking_lot::Mutex::new(file_watcher), - }); + let watcher = Arc::new(RealWatcher {}); watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher. @@ -622,18 +619,16 @@ impl Watcher for RealWatcher { impl Watcher for RealWatcher { fn add(&self, path: &Path) -> Result<()> { use notify::Watcher; - - self.fs_watcher - .lock() - .watch(path, notify::RecursiveMode::NonRecursive)?; - Ok(()) + Ok(watcher::global(|w| { + w.inotify + .lock() + .watch(path, notify::RecursiveMode::NonRecursive) + })??) } fn remove(&self, path: &Path) -> Result<()> { use notify::Watcher; - - self.fs_watcher.lock().unwatch(path)?; - Ok(()) + Ok(watcher::global(|w| w.inotify.lock().unwatch(path))??) } } @@ -1795,3 +1790,49 @@ mod tests { ); } } + +#[cfg(target_os = "linux")] +pub mod watcher { + use std::sync::OnceLock; + + use parking_lot::Mutex; + use util::ResultExt; + + pub struct GlobalWatcher { + // two mutexes because calling inotify.add triggers an inotify.event, which needs watchers. + pub(super) inotify: Mutex, + pub(super) watchers: Mutex>>, + } + + impl GlobalWatcher { + pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) { + self.watchers.lock().push(Box::new(cb)) + } + } + + static INOTIFY_INSTANCE: OnceLock> = + OnceLock::new(); + + fn handle_event(event: Result) { + let Some(event) = event.log_err() else { return }; + global::<()>(move |watcher| { + for f in watcher.watchers.lock().iter() { + f(&event) + } + }) + .log_err(); + } + + pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { + let result = INOTIFY_INSTANCE.get_or_init(|| { + notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { + inotify: Mutex::new(file_watcher), + watchers: Default::default(), + }) + }); + match result { + Ok(g) => Ok(f(g)), + Err(e) => Err(anyhow::anyhow!("{}", e)), + } + } +} diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 70d23dd287..297b1b4a5c 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -841,6 +841,10 @@ async fn test_write_file(cx: &mut TestAppContext) { ) .await .unwrap(); + + #[cfg(target_os = "linux")] + fs::watcher::global(|_| {}).unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; tree.flush_fs_events(cx).await; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 34fb5d12ba..8a2ace002d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -140,6 +140,25 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }) .detach(); + #[cfg(target_os = "linux")] + if let Err(e) = fs::watcher::global(|_| {}) { + let message = format!(db::indoc!{r#" + inotify_init returned {} + + This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux + "#}, e); + let prompt = cx.prompt(PromptLevel::Critical, "Could not start inotify", Some(&message), + &["Troubleshoot and Quit"]); + cx.spawn(|_, mut cx| async move { + if prompt.await == Ok(0) { + cx.update(|cx| { + cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify"); + cx.quit(); + }).ok(); + } + }).detach() + } + if let Some(specs) = cx.gpu_specs() { log::info!("Using GPU: {:?}", specs); if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() { diff --git a/docs/src/linux.md b/docs/src/linux.md index 537815d188..a677a0b408 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -122,3 +122,13 @@ All of these features are provided by XDG desktop portals, specifically: - `org.freedesktop.portal.Secret`, or `org.freedesktop.Secrets` Some window managers, such as `Hyprland`, don't provide a file picker by default. See [this list](https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces) as a starting point for alternatives. `KDE` doesn't implement the secret portal, installing `gnome-keyring` may solve this. + +### Could not start inotify + +Zed relies on inotify to watch your filesystem for changes. If you cannot start inotify then Zed will not work reliably. + +If you are seeing "too many open files" then first try `sysctl fs.inotify`. +* You should see that max_user_instances is 128 or higher (you can change the limit with `sudo sysctl fs.inotify.max_user_instances=1024`). Zed needs only 1 inotify instance. +* You should see that `max_user_watches` is 8000 or higher (you can change the limit with `sudo sysctl fs.inotify.max_user_watches=64000`). Zed needs one watch per directory in all your open projects + one per git repository + a handful more for settings, themes, keymaps, extensions. + +It is also possible that you are running out of file descriptors. You can check the limits with `ulimit` and update them by editing `/etc/security/limits.conf`.