mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
fs: Re-establish OS watches for registrations orphaned by a removed recursive watch
A watch registration whose path is covered by a recursive ancestor registration (native watches on macOS/Windows, poll watches everywhere) is recorded without an OS watch of its own. When the covering registration was removed - for example closing a window whose worktree root covered another window's repository git directory - the surviving registrations silently stopped receiving events until restart. On removal of an OS-backed watch, re-watch surviving descendant registrations that are no longer covered, shallowest first so a re-watched ancestor covers the orphans below it. Coverage checks now also require the covering ancestor to actually hold an OS watch.
This commit is contained in:
parent
9cdaff1492
commit
6c87e20de5
1 changed files with 149 additions and 7 deletions
|
|
@ -712,15 +712,23 @@ impl WatchPaths {
|
|||
}
|
||||
|
||||
/// True if a recursive registration on a strict ancestor already covers
|
||||
/// `path`. Only poll watches and native macOS/Windows watches are recursive.
|
||||
/// `path`. Only poll watches and native macOS/Windows watches are recursive,
|
||||
/// and only registrations backed by an OS watch provide coverage: a covered
|
||||
/// registration relies on some ancestor's OS watch itself, and that watch may
|
||||
/// since have been removed.
|
||||
fn covered_by_recursive_ancestor(&self, path: &SanitizedPath, mode: WatcherMode) -> bool {
|
||||
if mode != WatcherMode::Poll && !cfg!(any(target_os = "windows", target_os = "macos")) {
|
||||
return false;
|
||||
}
|
||||
path.as_path().ancestors().skip(1).any(|ancestor| {
|
||||
let ancestor = SanitizedPath::unchecked_new(ancestor);
|
||||
self.0.contains_key(&WatchKey::exact(ancestor))
|
||||
|| self.0.contains_key(&WatchKey::folded(ancestor))
|
||||
[WatchKey::exact(ancestor), WatchKey::folded(ancestor)]
|
||||
.iter()
|
||||
.any(|key| {
|
||||
self.0
|
||||
.get(key)
|
||||
.is_some_and(|registration| registration.has_os_watcher)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -979,6 +987,82 @@ impl GlobalWatcher {
|
|||
};
|
||||
drop(state);
|
||||
self.unwatch(path.as_path(), mode).log_err();
|
||||
self.rewatch_orphaned_descendants(&path, mode);
|
||||
}
|
||||
|
||||
/// Re-establishes OS watches for registrations that relied on a removed
|
||||
/// recursive watch on an ancestor path for their event delivery.
|
||||
///
|
||||
/// A registration covered by a recursive ancestor watch is recorded without
|
||||
/// an OS watch of its own. When the covering watch goes away (e.g. the
|
||||
/// project that owned it is closed), the surviving registrations would
|
||||
/// otherwise silently stop receiving events.
|
||||
fn rewatch_orphaned_descendants(&self, removed_path: &SanitizedPath, mode: WatcherMode) {
|
||||
if mode != WatcherMode::Poll && !cfg!(any(target_os = "windows", target_os = "macos")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut orphans = {
|
||||
let state = self.state.lock();
|
||||
let orphans = state
|
||||
.watchers
|
||||
.values()
|
||||
.filter(|registration| {
|
||||
registration.mode == mode
|
||||
&& registration.path.as_path() != removed_path.as_path()
|
||||
&& path_is_under(
|
||||
®istration.path,
|
||||
removed_path,
|
||||
matches!(registration.key, WatchKey::Folded(_)),
|
||||
)
|
||||
})
|
||||
.map(|registration| (registration.key.clone(), registration.path.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
orphans.into_iter().collect::<Vec<_>>()
|
||||
};
|
||||
// Watch shallower paths first, so that a re-watched ancestor covers the
|
||||
// orphans below it and they don't each get a redundant OS watch.
|
||||
orphans.sort_by_key(|(_, path)| path.as_path().components().count());
|
||||
|
||||
for (key, path) in orphans {
|
||||
let needs_watch = {
|
||||
let mut state = self.state.lock();
|
||||
let cooldown_active =
|
||||
mode == WatcherMode::Native && state.is_native_watch_limit_cooldown_active();
|
||||
let path_registrations = state.path_registrations(mode);
|
||||
!cooldown_active
|
||||
&& path_registrations
|
||||
.get_mut(&key)
|
||||
.is_some_and(|path_state| !path_state.has_os_watcher)
|
||||
&& !path_registrations.covered_by_recursive_ancestor(&path, mode)
|
||||
};
|
||||
if !needs_watch {
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.watch(path.as_path(), mode) {
|
||||
Ok(()) => {
|
||||
let mut state = self.state.lock();
|
||||
if let Some(path_state) = state.path_registrations(mode).get_mut(&key) {
|
||||
path_state.has_os_watcher = true;
|
||||
} else {
|
||||
// The registration was removed while we were watching; drop
|
||||
// the OS watch again so it doesn't leak.
|
||||
drop(state);
|
||||
self.unwatch(path.as_path(), mode).log_err();
|
||||
}
|
||||
}
|
||||
Err(error) if mode == WatcherMode::Native && is_max_files_watch_error(&error) => {
|
||||
self.start_native_watch_limit_cooldown(path.as_path());
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"failed to re-establish watch for {:?}: {error:#}",
|
||||
path.as_path()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> {
|
||||
|
|
@ -1222,7 +1306,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn covered_child_registration_is_not_unwatched_after_parent_is_removed() {
|
||||
fn covered_child_registration_is_rewatched_after_parent_is_removed() {
|
||||
let backend = Arc::new(Mutex::new(FakeWatchBackend::default()));
|
||||
let watcher = test_watcher(backend.clone());
|
||||
let parent = Arc::<Path>::from(Path::new("/repo"));
|
||||
|
|
@ -1237,12 +1321,70 @@ mod tests {
|
|||
.expect("add covered child watch")
|
||||
.expect("child watch registered");
|
||||
|
||||
// The child piggybacked on the parent's recursive watch; removing the
|
||||
// parent must give the child an OS watch of its own.
|
||||
watcher.remove(parent_registration);
|
||||
watcher.remove(child_registration);
|
||||
{
|
||||
let backend = backend.lock();
|
||||
assert_eq!(
|
||||
backend.watch_calls,
|
||||
&[parent.to_path_buf(), child.to_path_buf()]
|
||||
);
|
||||
assert_eq!(backend.unwatch_calls, &[parent.to_path_buf()]);
|
||||
}
|
||||
|
||||
watcher.remove(child_registration);
|
||||
let backend = backend.lock();
|
||||
assert_eq!(backend.watch_calls, &[parent.to_path_buf()]);
|
||||
assert_eq!(backend.unwatch_calls, &[parent.to_path_buf()]);
|
||||
assert_eq!(
|
||||
backend.unwatch_calls,
|
||||
&[parent.to_path_buf(), child.to_path_buf()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewatched_ancestor_covers_deeper_orphans() {
|
||||
let backend = Arc::new(Mutex::new(FakeWatchBackend::default()));
|
||||
let watcher = test_watcher(backend.clone());
|
||||
let root = Arc::<Path>::from(Path::new("/repo"));
|
||||
let mid = Arc::<Path>::from(Path::new("/repo/a"));
|
||||
let deep = Arc::<Path>::from(Path::new("/repo/a/b"));
|
||||
|
||||
let root_registration = watcher
|
||||
.add(root.as_ref().into(), WatcherMode::Poll, false, |_| {})
|
||||
.expect("add root watch")
|
||||
.expect("root watch registered");
|
||||
let mid_registration = watcher
|
||||
.add(mid.as_ref().into(), WatcherMode::Poll, false, |_| {})
|
||||
.expect("add covered mid watch")
|
||||
.expect("mid watch registered");
|
||||
let _deep_registration = watcher
|
||||
.add(deep.as_ref().into(), WatcherMode::Poll, false, |_| {})
|
||||
.expect("add covered deep watch")
|
||||
.expect("deep watch registered");
|
||||
|
||||
// Removing the root re-watches the shallowest orphan only; the deeper
|
||||
// orphan is covered by the re-watched ancestor's recursive watch.
|
||||
watcher.remove(root_registration);
|
||||
{
|
||||
let backend = backend.lock();
|
||||
assert_eq!(
|
||||
backend.watch_calls,
|
||||
&[root.to_path_buf(), mid.to_path_buf()]
|
||||
);
|
||||
assert_eq!(backend.unwatch_calls, &[root.to_path_buf()]);
|
||||
}
|
||||
|
||||
// Removing the mid registration orphans the deep one in turn.
|
||||
watcher.remove(mid_registration);
|
||||
let backend = backend.lock();
|
||||
assert_eq!(
|
||||
backend.watch_calls,
|
||||
&[root.to_path_buf(), mid.to_path_buf(), deep.to_path_buf()]
|
||||
);
|
||||
assert_eq!(
|
||||
backend.unwatch_calls,
|
||||
&[root.to_path_buf(), mid.to_path_buf()]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue