From d3fc00d5a0f6e493bfe1ac4a8f231a59f408626c Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Tue, 7 Jan 2025 23:50:22 +0530 Subject: [PATCH] windows: Fix fs watch when file doesn't exist or is a symlink (#22660) Closes #22659 More context can be found in attached issue. This is specific to Windows: 1. Add parent directory watching for fs watch when the file doesn't exist. For example, when Zed is first launched and `settings.json` isn't there. 2. Add proper symlink handling for fs watch. For example, when `settings.json` is a symlink. This is exactly same as how we handle it on Linux. Release Notes: - Fixed an issue where items on the Welcome page could not be toggled on Windows, either on first launch or when `settings.json` is a symlink. --- crates/fs/src/fs.rs | 61 ++----------------- .../src/{linux_watcher.rs => fs_watcher.rs} | 25 ++++---- crates/worktree/src/worktree_tests.rs | 4 +- crates/zed/src/zed.rs | 38 ++++++++++-- 4 files changed, 56 insertions(+), 72 deletions(-) rename crates/fs/src/{linux_watcher.rs => fs_watcher.rs} (83%) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 59302f1d2f..a88114ed4b 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,8 +1,8 @@ #[cfg(target_os = "macos")] mod mac_watcher; -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub mod linux_watcher; +#[cfg(not(target_os = "macos"))] +pub mod fs_watcher; use anyhow::{anyhow, Result}; use git::GitHostingProviderRegistry; @@ -700,7 +700,7 @@ impl Fs for RealFs { ) } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] + #[cfg(not(target_os = "macos"))] async fn watch( &self, path: &Path, @@ -710,10 +710,11 @@ impl Fs for RealFs { Arc, ) { use parking_lot::Mutex; + use util::paths::SanitizedPath; let (tx, rx) = smol::channel::unbounded(); let pending_paths: Arc>> = Default::default(); - let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone())); + let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); if watcher.add(path).is_err() { // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. @@ -731,7 +732,7 @@ impl Fs for RealFs { if let Some(parent) = path.parent() { target = parent.join(target); if let Ok(canonical) = self.canonicalize(&target).await { - target = canonical; + target = SanitizedPath::from(canonical).as_path().to_path_buf(); } } } @@ -758,56 +759,6 @@ impl Fs for RealFs { ) } - #[cfg(target_os = "windows")] - async fn watch( - &self, - path: &Path, - _latency: Duration, - ) -> ( - Pin>>>, - Arc, - ) { - use notify::{EventKind, Watcher}; - - let (tx, rx) = smol::channel::unbounded(); - - let mut file_watcher = notify::recommended_watcher({ - let tx = tx.clone(); - move |event: Result| { - if let Some(event) = event.log_err() { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - - tx.try_send( - event - .paths - .into_iter() - .map(|path| PathEvent { path, kind }) - .collect::>(), - ) - .ok(); - } - } - }) - .expect("Could not start file watcher"); - - file_watcher - .watch(path, notify::RecursiveMode::Recursive) - .log_err(); - - ( - Box::pin(rx.chain(futures::stream::once(async move { - drop(file_watcher); - vec![] - }))), - Arc::new(RealWatcher {}), - ) - } - fn open_repo(&self, dotgit_path: &Path) -> Option> { // with libgit2, we can open git repo from an existing work dir // https://libgit2.org/docs/reference/main/repository/git_repository_open.html diff --git a/crates/fs/src/linux_watcher.rs b/crates/fs/src/fs_watcher.rs similarity index 83% rename from crates/fs/src/linux_watcher.rs rename to crates/fs/src/fs_watcher.rs index cb2ac4826f..2a6c309dc9 100644 --- a/crates/fs/src/linux_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -5,12 +5,12 @@ use util::ResultExt; use crate::{PathEvent, PathEventKind, Watcher}; -pub struct LinuxWatcher { +pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, } -impl LinuxWatcher { +impl FsWatcher { pub fn new( tx: smol::channel::Sender<()>, pending_path_events: Arc>>, @@ -22,7 +22,7 @@ impl LinuxWatcher { } } -impl Watcher for LinuxWatcher { +impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> gpui::Result<()> { let root_path = path.to_path_buf(); @@ -69,7 +69,7 @@ impl Watcher for LinuxWatcher { })?; global(|g| { - g.inotify + g.watcher .lock() .watch(path, notify::RecursiveMode::NonRecursive) })??; @@ -79,16 +79,18 @@ impl Watcher for LinuxWatcher { fn remove(&self, path: &std::path::Path) -> gpui::Result<()> { use notify::Watcher; - Ok(global(|w| w.inotify.lock().unwatch(path))??) + Ok(global(|w| w.watcher.lock().unwatch(path))??) } } pub struct GlobalWatcher { - // two mutexes because calling inotify.add triggers an inotify.event, which needs watchers. + // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. #[cfg(target_os = "linux")] - pub(super) inotify: Mutex, + pub(super) watcher: Mutex, #[cfg(target_os = "freebsd")] - pub(super) inotify: Mutex, + pub(super) watcher: Mutex, + #[cfg(target_os = "windows")] + pub(super) watcher: Mutex, pub(super) watchers: Mutex>>, } @@ -98,7 +100,8 @@ impl GlobalWatcher { } } -static INOTIFY_INSTANCE: OnceLock> = OnceLock::new(); +static FS_WATCHER_INSTANCE: OnceLock> = + OnceLock::new(); fn handle_event(event: Result) { let Some(event) = event.log_err() else { return }; @@ -111,9 +114,9 @@ fn handle_event(event: Result) { } pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { - let result = INOTIFY_INSTANCE.get_or_init(|| { + let result = FS_WATCHER_INSTANCE.get_or_init(|| { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { - inotify: Mutex::new(file_watcher), + watcher: Mutex::new(file_watcher), watchers: Default::default(), }) }); diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 4df3d98469..eebb5f9360 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -854,8 +854,8 @@ async fn test_write_file(cx: &mut TestAppContext) { .await .unwrap(); - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - fs::linux_watcher::global(|_| {}).unwrap(); + #[cfg(not(target_os = "macos"))] + fs::fs_watcher::global(|_| {}).unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f005768600..4e8dd1bcba 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -153,8 +153,8 @@ pub fn initialize_workspace( }) .detach(); - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - initialize_linux_file_watcher(cx); + #[cfg(not(target_os = "macos"))] + initialize_file_watcher(cx); if let Some(specs) = cx.gpu_specs() { log::info!("Using GPU: {:?}", specs); @@ -235,8 +235,8 @@ fn feature_gate_zed_pro_actions(cx: &mut AppContext) { } #[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn initialize_linux_file_watcher(cx: &mut ViewContext) { - if let Err(e) = fs::linux_watcher::global(|_| {}) { +fn initialize_file_watcher(cx: &mut ViewContext) { + if let Err(e) = fs::fs_watcher::global(|_| {}) { let message = format!( db::indoc! {r#" inotify_init returned {} @@ -264,6 +264,36 @@ fn initialize_linux_file_watcher(cx: &mut ViewContext) { } } +#[cfg(target_os = "windows")] +fn initialize_file_watcher(cx: &mut ViewContext) { + if let Err(e) = fs::fs_watcher::global(|_| {}) { + let message = format!( + db::indoc! {r#" + ReadDirectoryChangesW initialization failed: {} + + This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows + "#}, + e + ); + let prompt = cx.prompt( + PromptLevel::Critical, + "Could not start ReadDirectoryChangesW", + 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/windows"); + cx.quit() + }) + .ok(); + } + }) + .detach() + } +} + fn show_software_emulation_warning_if_needed( specs: gpui::GpuSpecs, cx: &mut ViewContext,