Skip to content

Commit

Permalink
windows: Fix fs watch when file doesn't exist or is a symlink (#22660)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
0xtimsb authored Jan 7, 2025
1 parent d58f006 commit d3fc00d
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 72 deletions.
61 changes: 6 additions & 55 deletions crates/fs/src/fs.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -710,10 +710,11 @@ impl Fs for RealFs {
Arc<dyn Watcher>,
) {
use parking_lot::Mutex;
use util::paths::SanitizedPath;

let (tx, rx) = smol::channel::unbounded();
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = 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.
Expand All @@ -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();
}
}
}
Expand All @@ -758,56 +759,6 @@ impl Fs for RealFs {
)
}

#[cfg(target_os = "windows")]
async fn watch(
&self,
path: &Path,
_latency: Duration,
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
use notify::{EventKind, Watcher};

let (tx, rx) = smol::channel::unbounded();

let mut file_watcher = notify::recommended_watcher({
let tx = tx.clone();
move |event: Result<notify::Event, _>| {
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::<Vec<_>>(),
)
.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<Arc<dyn GitRepository>> {
// with libgit2, we can open git repo from an existing work dir
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html
Expand Down
25 changes: 14 additions & 11 deletions crates/fs/src/linux_watcher.rs → crates/fs/src/fs_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<Vec<PathEvent>>>,
}

impl LinuxWatcher {
impl FsWatcher {
pub fn new(
tx: smol::channel::Sender<()>,
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
Expand All @@ -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();

Expand Down Expand Up @@ -69,7 +69,7 @@ impl Watcher for LinuxWatcher {
})?;

global(|g| {
g.inotify
g.watcher
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)
})??;
Expand All @@ -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<notify::INotifyWatcher>,
pub(super) watcher: Mutex<notify::INotifyWatcher>,
#[cfg(target_os = "freebsd")]
pub(super) inotify: Mutex<notify::KqueueWatcher>,
pub(super) watcher: Mutex<notify::KqueueWatcher>,
#[cfg(target_os = "windows")]
pub(super) watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
pub(super) watchers: Mutex<Vec<Box<dyn Fn(&notify::Event) + Send + Sync>>>,
}

Expand All @@ -98,7 +100,8 @@ impl GlobalWatcher {
}
}

static INOTIFY_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> = OnceLock::new();
static FS_WATCHER_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 };
Expand All @@ -111,9 +114,9 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
}

pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
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(),
})
});
Expand Down
4 changes: 2 additions & 2 deletions crates/worktree/src/worktree_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 34 additions & 4 deletions crates/zed/src/zed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Workspace>) {
if let Err(e) = fs::linux_watcher::global(|_| {}) {
fn initialize_file_watcher(cx: &mut ViewContext<Workspace>) {
if let Err(e) = fs::fs_watcher::global(|_| {}) {
let message = format!(
db::indoc! {r#"
inotify_init returned {}
Expand Down Expand Up @@ -264,6 +264,36 @@ fn initialize_linux_file_watcher(cx: &mut ViewContext<Workspace>) {
}
}

#[cfg(target_os = "windows")]
fn initialize_file_watcher(cx: &mut ViewContext<Workspace>) {
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<Workspace>,
Expand Down

0 comments on commit d3fc00d

Please sign in to comment.