@@ -140,10 +140,7 @@ pub struct RealFs {
git_binary_path: Option<PathBuf>,
}
-pub struct RealWatcher {
- #[cfg(target_os = "linux")]
- fs_watcher: parking_lot::Mutex<notify::INotifyWatcher>,
-}
+pub struct RealWatcher {}
impl RealFs {
pub fn new(
@@ -472,29 +469,29 @@ impl Fs for RealFs {
let pending_paths: Arc<Mutex<Vec<PathBuf>>> = 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<notify::Event, _>| {
- 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::<Vec<_>>();
+ 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<notify::INotifyWatcher>,
+ pub(super) watchers: Mutex<Vec<Box<dyn Fn(¬ify::Event) + Send + Sync>>>,
+ }
+
+ 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<anyhow::Result<GlobalWatcher, notify::Error>> =
+ OnceLock::new();
+
+ fn handle_event(event: Result<notify::Event, notify::Error>) {
+ 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<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
+ 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)),
+ }
+ }
+}
@@ -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;
@@ -140,6 +140,25 @@ pub fn initialize_workspace(app_state: Arc<AppState>, 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() {
@@ -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`.