mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-25 04:54:07 +00:00
fix: platform-specific watcher for macOS FSEvents + Windows RDCW
macOS: raw notify with custom 50ms debounce thread. FSEvents provides complete event kinds (Create/Remove/Modify) without needing a file cache, avoiding the debouncer-full's Create+Remove cancellation bug. Windows/Linux: notify-debouncer-full with RecommendedCache. RDCW needs the file cache to detect cross-directory moves and renames. The cache also handles RDCW's limited event granularity. Both platforms share the same convert_event logic for mapping notify EventKinds to VfsEvents. All 489 tests passing locally (macOS).
This commit is contained in:
parent
b8bc7dda5b
commit
a727881e6f
3 changed files with 137 additions and 47 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -1030,6 +1030,15 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "file-id"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.27"
|
version = "0.2.27"
|
||||||
|
|
@ -1911,6 +1920,7 @@ dependencies = [
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
|
"notify-debouncer-full",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
@ -1980,6 +1990,19 @@ dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-debouncer-full"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302"
|
||||||
|
dependencies = [
|
||||||
|
"file-id",
|
||||||
|
"log",
|
||||||
|
"notify",
|
||||||
|
"notify-types",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-types"
|
name = "notify-types"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,8 @@ log = "0.4"
|
||||||
notify = "8.2"
|
notify = "8.2"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
|
notify-debouncer-full = "0.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.26.0"
|
tempfile = "3.26.0"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use std::{collections::HashSet, io, thread};
|
use std::{collections::HashSet, io, thread};
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode};
|
use notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode};
|
||||||
use notify::{RecursiveMode, Watcher};
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
|
||||||
|
|
||||||
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||||
|
|
||||||
/// Critical errors from the file watcher that indicate watching is no longer reliable.
|
/// Critical errors from the file watcher that indicate watching is no longer reliable.
|
||||||
|
|
@ -52,7 +53,10 @@ pub type CriticalErrorHandler = Box<dyn Fn(WatcherCriticalError) -> bool + Send
|
||||||
|
|
||||||
/// `VfsBackend` that uses `std::fs` and the `notify` crate.
|
/// `VfsBackend` that uses `std::fs` and the `notify` crate.
|
||||||
pub struct StdBackend {
|
pub struct StdBackend {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
watcher: notify::RecommendedWatcher,
|
watcher: notify::RecommendedWatcher,
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
debouncer: Debouncer<notify::RecommendedWatcher, RecommendedCache>,
|
||||||
watcher_receiver: Receiver<VfsEvent>,
|
watcher_receiver: Receiver<VfsEvent>,
|
||||||
watches: HashSet<PathBuf>,
|
watches: HashSet<PathBuf>,
|
||||||
recursive_watches: HashSet<PathBuf>,
|
recursive_watches: HashSet<PathBuf>,
|
||||||
|
|
@ -102,66 +106,118 @@ impl StdBackend {
|
||||||
pub fn new_with_error_handler(error_handler: CriticalErrorHandler) -> StdBackend {
|
pub fn new_with_error_handler(error_handler: CriticalErrorHandler) -> StdBackend {
|
||||||
let (event_tx, event_rx) = crossbeam_channel::unbounded();
|
let (event_tx, event_rx) = crossbeam_channel::unbounded();
|
||||||
let (error_tx, error_rx) = crossbeam_channel::unbounded();
|
let (error_tx, error_rx) = crossbeam_channel::unbounded();
|
||||||
let (raw_tx, raw_rx) = std::sync::mpsc::channel::<Result<notify::Event, notify::Error>>();
|
|
||||||
|
|
||||||
let watcher = notify::RecommendedWatcher::new(
|
#[cfg(target_os = "macos")]
|
||||||
move |result| {
|
let watcher = {
|
||||||
let _ = raw_tx.send(result);
|
let (raw_tx, raw_rx) =
|
||||||
},
|
std::sync::mpsc::channel::<Result<notify::Event, notify::Error>>();
|
||||||
notify::Config::default(),
|
|
||||||
)
|
|
||||||
.expect("Failed to create file watcher");
|
|
||||||
|
|
||||||
let debounce_ms = Duration::from_millis(50);
|
let w = notify::RecommendedWatcher::new(
|
||||||
|
move |result| {
|
||||||
|
let _ = raw_tx.send(result);
|
||||||
|
},
|
||||||
|
notify::Config::default(),
|
||||||
|
)
|
||||||
|
.expect("Failed to create file watcher");
|
||||||
|
|
||||||
thread::spawn(move || {
|
let debounce_ms = std::time::Duration::from_millis(50);
|
||||||
let mut pending: HashMap<PathBuf, VfsEvent> = HashMap::new();
|
|
||||||
let mut last_event = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
thread::spawn(move || {
|
||||||
match raw_rx.recv_timeout(debounce_ms) {
|
use std::collections::HashMap;
|
||||||
Ok(Ok(event)) => {
|
use std::time::Instant;
|
||||||
last_event = Instant::now();
|
|
||||||
for vfs_event in Self::convert_event(&event) {
|
let mut pending: HashMap<PathBuf, VfsEvent> = HashMap::new();
|
||||||
let path = match &vfs_event {
|
let mut last_event = Instant::now();
|
||||||
VfsEvent::Create(p) | VfsEvent::Write(p) | VfsEvent::Remove(p) => {
|
|
||||||
p.clone()
|
loop {
|
||||||
}
|
match raw_rx.recv_timeout(debounce_ms) {
|
||||||
|
Ok(Ok(event)) => {
|
||||||
|
last_event = Instant::now();
|
||||||
|
for vfs_event in Self::convert_event(&event) {
|
||||||
|
let path = match &vfs_event {
|
||||||
|
VfsEvent::Create(p)
|
||||||
|
| VfsEvent::Write(p)
|
||||||
|
| VfsEvent::Remove(p) => p.clone(),
|
||||||
|
};
|
||||||
|
pending.insert(path, vfs_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
let critical_err = WatcherCriticalError::WatcherError {
|
||||||
|
error: format!("{:?}", error.kind),
|
||||||
|
path: error.paths.first().cloned(),
|
||||||
};
|
};
|
||||||
pending.insert(path, vfs_event);
|
let _ = error_tx.send(critical_err.clone());
|
||||||
|
if error_handler(critical_err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
|
||||||
|
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
|
||||||
}
|
}
|
||||||
Ok(Err(error)) => {
|
|
||||||
let critical_err = WatcherCriticalError::WatcherError {
|
|
||||||
error: format!("{:?}", error.kind),
|
|
||||||
path: error.paths.first().cloned(),
|
|
||||||
};
|
|
||||||
let _ = error_tx.send(critical_err.clone());
|
|
||||||
if error_handler(critical_err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
|
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pending.is_empty() && last_event.elapsed() >= debounce_ms {
|
if !pending.is_empty() && last_event.elapsed() >= debounce_ms {
|
||||||
for (_, vfs_event) in pending.drain() {
|
for (_, vfs_event) in pending.drain() {
|
||||||
if let Err(err) = event_tx.send(vfs_event) {
|
if let Err(err) = event_tx.send(vfs_event) {
|
||||||
let critical_err =
|
let critical_err =
|
||||||
WatcherCriticalError::ChannelSendFailed(err.to_string());
|
WatcherCriticalError::ChannelSendFailed(err.to_string());
|
||||||
|
let _ = error_tx.send(critical_err.clone());
|
||||||
|
if error_handler(critical_err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
w
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let debouncer = {
|
||||||
|
let debounce_timeout = std::time::Duration::from_millis(50);
|
||||||
|
|
||||||
|
new_debouncer(
|
||||||
|
debounce_timeout,
|
||||||
|
None,
|
||||||
|
move |result: DebounceEventResult| match result {
|
||||||
|
Ok(events) => {
|
||||||
|
for event in events {
|
||||||
|
for vfs_event in Self::convert_event(&event.event) {
|
||||||
|
if let Err(err) = event_tx.send(vfs_event) {
|
||||||
|
let critical_err =
|
||||||
|
WatcherCriticalError::ChannelSendFailed(err.to_string());
|
||||||
|
let _ = error_tx.send(critical_err.clone());
|
||||||
|
if error_handler(critical_err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(errors) => {
|
||||||
|
for error in errors {
|
||||||
|
let critical_err = WatcherCriticalError::WatcherError {
|
||||||
|
error: format!("{:?}", error.kind),
|
||||||
|
path: error.paths.first().cloned(),
|
||||||
|
};
|
||||||
let _ = error_tx.send(critical_err.clone());
|
let _ = error_tx.send(critical_err.clone());
|
||||||
if error_handler(critical_err) {
|
if error_handler(critical_err) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
});
|
.expect("Failed to create file watcher debouncer")
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
watcher,
|
watcher,
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
debouncer,
|
||||||
watcher_receiver: event_rx,
|
watcher_receiver: event_rx,
|
||||||
watches: HashSet::new(),
|
watches: HashSet::new(),
|
||||||
recursive_watches: HashSet::new(),
|
recursive_watches: HashSet::new(),
|
||||||
|
|
@ -333,7 +389,11 @@ impl VfsBackend for StdBackend {
|
||||||
} else {
|
} else {
|
||||||
RecursiveMode::NonRecursive
|
RecursiveMode::NonRecursive
|
||||||
};
|
};
|
||||||
match self.watcher.watch(path, mode) {
|
#[cfg(target_os = "macos")]
|
||||||
|
let result = self.watcher.watch(path, mode);
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let result = self.debouncer.watch(path, mode);
|
||||||
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Watching path ({}): {}",
|
"Watching path ({}): {}",
|
||||||
|
|
@ -360,7 +420,11 @@ impl VfsBackend for StdBackend {
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
let was_watched = self.watches.contains(path);
|
let was_watched = self.watches.contains(path);
|
||||||
|
|
||||||
match self.watcher.unwatch(path) {
|
#[cfg(target_os = "macos")]
|
||||||
|
let result = self.watcher.unwatch(path);
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let result = self.debouncer.unwatch(path);
|
||||||
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if was_watched {
|
if was_watched {
|
||||||
log::info!("Unwatched path: {}", path.display());
|
log::info!("Unwatched path: {}", path.display());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue