Introduce cross-platform file-watching (#6855)

This adds cross-platform file-watching via the
[Notify](https://github.com/notify-rs/notify) crate. The previous
fs-events implementation is now only used on MacOS, and on other
platforms Notify is used. The watching function interface is the same.

Related to #5391 #5395 #5394.

Release Notes:

- N/A
This commit is contained in:
Amin Yahyaabadi 2024-01-29 12:18:10 -08:00 committed by GitHub
parent b29f45ea68
commit 1313402a6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 141 additions and 12 deletions

72
Cargo.lock generated
View File

@ -2737,6 +2737,7 @@ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
"log", "log",
"notify",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"regex", "regex",
"rope", "rope",
@ -2756,7 +2757,7 @@ name = "fsevent"
version = "2.0.2" version = "2.0.2"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"fsevent-sys", "fsevent-sys 3.1.0",
"parking_lot 0.11.2", "parking_lot 0.11.2",
"tempfile", "tempfile",
] ]
@ -2770,6 +2771,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "fuchsia-zircon" name = "fuchsia-zircon"
version = "0.3.3" version = "0.3.3"
@ -3509,6 +3519,26 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "install_cli" name = "install_cli"
version = "0.1.0" version = "0.1.0"
@ -3726,6 +3756,26 @@ dependencies = [
"winapi-build", "winapi-build",
] ]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.8.3" version = "0.8.3"
@ -4281,6 +4331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@ -4534,6 +4585,25 @@ dependencies = [
"util", "util",
] ]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.1",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.8",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.7" version = "0.3.7"

View File

@ -20,7 +20,6 @@ anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
futures.workspace = true futures.workspace = true
tempfile = "3" tempfile = "3"
fsevent = { path = "../fsevent" }
lazy_static.workspace = true lazy_static.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
smol.workspace = true smol.workspace = true
@ -35,6 +34,12 @@ time.workspace = true
gpui = { path = "../gpui", optional = true} gpui = { path = "../gpui", optional = true}
[target.'cfg(target_os = "macos")'.dependencies]
fsevent = { path = "../fsevent" }
[target.'cfg(not(target_os = "macos"))'.dependencies]
notify = "6.1.1"
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }

View File

@ -1,7 +1,16 @@
pub mod repository; pub mod repository;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
#[cfg(target_os = "macos")]
pub use fsevent::Event;
#[cfg(target_os = "macos")]
use fsevent::EventStream; use fsevent::EventStream;
#[cfg(not(target_os = "macos"))]
pub use notify::Event;
#[cfg(not(target_os = "macos"))]
use notify::{Config, Watcher};
use futures::{future::BoxFuture, Stream, StreamExt}; use futures::{future::BoxFuture, Stream, StreamExt};
use git2::Repository as LibGitRepository; use git2::Repository as LibGitRepository;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -48,11 +57,13 @@ pub trait Fs: Send + Sync {
&self, &self,
path: &Path, path: &Path,
) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>; ) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>>;
async fn watch( async fn watch(
&self, &self,
path: &Path, path: &Path,
latency: Duration, latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>; ) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>; fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
fn is_fake(&self) -> bool; fn is_fake(&self) -> bool;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -251,11 +262,12 @@ impl Fs for RealFs {
Ok(Box::pin(result)) Ok(Box::pin(result))
} }
#[cfg(target_os = "macos")]
async fn watch( async fn watch(
&self, &self,
path: &Path, path: &Path,
latency: Duration, latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> { ) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> {
let (tx, rx) = smol::channel::unbounded(); let (tx, rx) = smol::channel::unbounded();
let (stream, handle) = EventStream::new(&[path], latency); let (stream, handle) = EventStream::new(&[path], latency);
std::thread::spawn(move || { std::thread::spawn(move || {
@ -267,6 +279,35 @@ impl Fs for RealFs {
}))) })))
} }
#[cfg(not(target_os = "macos"))]
async fn watch(
&self,
path: &Path,
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> {
let (tx, rx) = smol::channel::unbounded();
let mut watcher = notify::recommended_watcher(move |res| match res {
Ok(event) => {
let _ = tx.try_send(vec![event]);
}
Err(err) => {
eprintln!("watch error: {:?}", err);
}
})
.unwrap();
watcher
.configure(Config::default().with_poll_interval(latency))
.unwrap();
watcher
.watch(path, notify::RecursiveMode::Recursive)
.unwrap();
Box::pin(rx)
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> { fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
LibGitRepository::open(&dotgit_path) LibGitRepository::open(&dotgit_path)
.log_err() .log_err()
@ -284,6 +325,20 @@ impl Fs for RealFs {
} }
} }
#[cfg(target_os = "macos")]
pub fn fs_events_paths(events: Vec<Event>) -> Vec<PathBuf> {
events.into_iter().map(|event| event.path).collect()
}
#[cfg(not(target_os = "macos"))]
pub fn fs_events_paths(events: Vec<Event>) -> Vec<PathBuf> {
events
.into_iter()
.map(|event| event.paths.into_iter())
.flatten()
.collect()
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub struct FakeFs { pub struct FakeFs {
// Use an unfair lock to ensure tests are deterministic. // Use an unfair lock to ensure tests are deterministic.

View File

@ -3221,10 +3221,7 @@ impl BackgroundScanner {
} }
} }
async fn run( async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fs::Event>>>>) {
&mut self,
mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>,
) {
use futures::FutureExt as _; use futures::FutureExt as _;
// Populate ignores above the root. // Populate ignores above the root.
@ -3271,9 +3268,10 @@ impl BackgroundScanner {
// have the previous state loaded yet. // have the previous state loaded yet.
self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan; self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) { if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) {
let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>(); let mut paths = fs::fs_events_paths(events);
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
paths.extend(more_events.into_iter().map(|e| e.path)); paths.extend(fs::fs_events_paths(more_events));
} }
self.process_events(paths).await; self.process_events(paths).await;
} }
@ -3312,9 +3310,10 @@ impl BackgroundScanner {
events = fs_events_rx.next().fuse() => { events = fs_events_rx.next().fuse() => {
let Some(events) = events else { break }; let Some(events) = events else { break };
let mut paths = events.into_iter().map(|e| e.path).collect::<Vec<_>>(); let mut paths = fs::fs_events_paths(events);
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
paths.extend(more_events.into_iter().map(|e| e.path)); paths.extend(fs::fs_events_paths(more_events));
} }
self.process_events(paths.clone()).await; self.process_events(paths.clone()).await;
} }