inotify alert (#15027)

Release Notes:

- linux: Show an error and troubleshooting steps for inotify limits
(#10310)
This commit is contained in:
Conrad Irwin 2024-07-23 14:21:56 -06:00 committed by GitHub
parent 41a3e78b1e
commit b0c525af5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 29 deletions

View File

@ -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: &notify::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(&notify::Event) + Send + Sync>>>,
}
impl GlobalWatcher {
pub(super) fn add(&self, cb: impl Fn(&notify::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)),
}
}
}

View File

@ -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;

View File

@ -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() {

View File

@ -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`.