From 4b38c070fb05d1812959b6ecf5aba267f21c6578 Mon Sep 17 00:00:00 2001 From: Joe Date: Sat, 7 Mar 2026 10:40:32 -0600 Subject: [PATCH] Refactor VFS watch logic for macOS and Linux/Windows compatibility - Enhanced the VFS watch setup in ServeSession to differentiate between macOS and other platforms, ensuring proper handling of file and directory watches. - Improved clarity by restructuring the logic for watching paths, addressing platform-specific requirements and optimizing resource usage. - Maintained non-recursive watching for ignored directories while ensuring all relevant files are monitored effectively. --- src/serve_session.rs | 67 ++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/serve_session.rs b/src/serve_session.rs index fb0f1427..6b0efe7b 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -384,38 +384,49 @@ impl ServeSession { use walkdir::WalkDir; let watch_start = Instant::now(); - // Walk each $path root, skipping directories that match - // globIgnorePaths, and watch each non-ignored directory - // individually (non-recursive). This avoids creating kqueue - // FDs for ignored subtrees on macOS. - let passes_ignore = |entry: &walkdir::DirEntry| -> bool { - watch_ignore_rules - .iter() - .all(|rule: &PathIgnoreRule| rule.passes(entry.path())) - }; - vfs.set_watch_recursive(false); let mut watch_count: usize = 0; - for root in &path_roots { - for entry in WalkDir::new(root) - .follow_links(true) - .into_iter() - .filter_entry(&passes_ignore) - .flatten() - { - // Watch both files and directories. On macOS kqueue, - // file watches are required because directory watches - // only fire for structural changes. On Linux/Windows, - // file watches prevent a debouncer race where read_raw's - // auto-registered file watch conflicts with the directory - // watch during rename-overwrite (atomic save pattern). - let _ = vfs.watch(entry.path()); + + if cfg!(target_os = "macos") { + // macOS kqueue: one FD per watched path. Walk each $path + // root, skip globIgnorePaths subtrees, and watch every + // non-ignored file and directory non-recursively. File + // watches are required because kqueue directory watches + // only fire for structural changes, not child content edits. + let passes_ignore = |entry: &walkdir::DirEntry| -> bool { + watch_ignore_rules + .iter() + .all(|rule: &PathIgnoreRule| rule.passes(entry.path())) + }; + vfs.set_watch_recursive(false); + for root in &path_roots { + for entry in WalkDir::new(root) + .follow_links(true) + .into_iter() + .filter_entry(&passes_ignore) + .flatten() + { + let _ = vfs.watch(entry.path()); + watch_count += 1; + } + } + let _ = vfs.watch(&project_folder); + watch_count += 1; + vfs.set_watch_recursive(true); + } else { + // Linux/Windows: inotify uses a single FD for all watches, + // RDCW uses one handle per directory. Recursive watches are + // cheap and directory watches already cover child file + // events. globIgnorePaths filtering is handled by the + // change processor's event filter. + for root in &path_roots { + let _ = vfs.watch(root); watch_count += 1; } + vfs.set_watch_recursive(false); + let _ = vfs.watch(&project_folder); + vfs.set_watch_recursive(true); + watch_count += 1; } - // Watch project folder non-recursively for project file edits. - let _ = vfs.watch(&project_folder); - watch_count += 1; - vfs.set_watch_recursive(true); log::debug!( "Set up {} watches in {:.1?}", watch_count,