Compare commits
2 Commits
shell-quot
...
nia/notify
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee3087090f | ||
|
|
c9d6a2ca24 |
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -6412,6 +6412,7 @@ dependencies = [
|
||||
"git",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"inotify 0.11.0",
|
||||
"is_executable",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8256,8 +8257,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"futures-core",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10448,7 +10451,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"fsevent-sys 4.1.0",
|
||||
@@ -10476,7 +10480,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "now"
|
||||
|
||||
@@ -775,8 +775,8 @@ features = [
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
#notify = { path = "../notify/notify" }
|
||||
#notify-types = { path = "../notify/notify-types" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
||||
|
||||
|
||||
@@ -40,9 +40,12 @@ fsevent.workspace = true
|
||||
objc.workspace = true
|
||||
cocoa = "0.26"
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "linux")))'.dependencies]
|
||||
notify = "8.2.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
inotify = "0.11.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_watcher;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_watcher;
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
pub mod fs_watcher;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
@@ -420,7 +423,7 @@ impl RealFs {
|
||||
pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
|
||||
Self {
|
||||
bundled_git_binary_path: git_binary_path,
|
||||
executor,
|
||||
executor: executor.clone(),
|
||||
next_job_id: Arc::new(AtomicUsize::new(0)),
|
||||
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
@@ -1003,7 +1006,43 @@ impl Fs for RealFs {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
use util::{ResultExt as _, paths::SanitizedPath};
|
||||
|
||||
let (watcher, stream) = linux_watcher::LinuxWatcher::init(self.executor.clone());
|
||||
watcher.add(path).unwrap();
|
||||
|
||||
// Check if path is a symlink and follow the target parent
|
||||
if let Some(mut target) = self.read_link(path).await.ok() {
|
||||
dbg!("we got a symlink");
|
||||
log::trace!("watch symlink {path:?} -> {target:?}");
|
||||
// Check if symlink target is relative path, if so make it absolute
|
||||
if target.is_relative()
|
||||
&& let Some(parent) = path.parent()
|
||||
{
|
||||
target = parent.join(target);
|
||||
if let Ok(canonical) = self.canonicalize(&target).await {
|
||||
target = SanitizedPath::new(&canonical).as_path().to_path_buf();
|
||||
}
|
||||
}
|
||||
watcher.add(&target).ok();
|
||||
if let Some(parent) = target.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
(stream, Arc::new(watcher) as _)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
@@ -1016,6 +1055,7 @@ impl Fs for RealFs {
|
||||
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||
|
||||
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
|
||||
|
||||
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
|
||||
@@ -3378,12 +3418,7 @@ mod tests {
|
||||
async fn test_realfs_atomic_write(executor: BackgroundExecutor) {
|
||||
// With the file handle still open, the file should be replaced
|
||||
// https://github.com/zed-industries/zed/issues/30054
|
||||
let fs = RealFs {
|
||||
bundled_git_binary_path: None,
|
||||
executor,
|
||||
next_job_id: Arc::new(AtomicUsize::new(0)),
|
||||
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
};
|
||||
let fs = RealFs::new(None, executor);
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_to_be_replaced = temp_dir.path().join("file.txt");
|
||||
let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap();
|
||||
@@ -3398,12 +3433,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) {
|
||||
let fs = RealFs {
|
||||
bundled_git_binary_path: None,
|
||||
executor,
|
||||
next_job_id: Arc::new(AtomicUsize::new(0)),
|
||||
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
};
|
||||
let fs = RealFs::new(None, executor);
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file_to_be_replaced = temp_dir.path().join("file.txt");
|
||||
smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use notify::EventKind;
|
||||
#[cfg(target_os = "linux")]
|
||||
use notify::{INotifyWatcher, Watcher};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
@@ -249,6 +251,11 @@ fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||
|
||||
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
||||
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let watcher = INotifyWatcher::new(event_handler, config);
|
||||
}
|
||||
#[cfg(not(linux))]
|
||||
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
||||
state: Mutex::new(WatcherState {
|
||||
watchers: Default::default(),
|
||||
|
||||
218
crates/fs/src/linux_watcher.rs
Normal file
218
crates/fs/src/linux_watcher.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
pin::Pin,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{PathEvent, Watcher};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use futures::Stream;
|
||||
use gpui::{BackgroundExecutor, Task};
|
||||
use inotify::{Inotify, WatchMask};
|
||||
use smol::{
|
||||
channel::{Receiver, Sender},
|
||||
future::block_on,
|
||||
};
|
||||
|
||||
pub struct LinuxWatcher {
|
||||
inotify: InotifyWatcher,
|
||||
poller: PollWatcher,
|
||||
//task: Task<Result<Never>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WatcherStateUpdate {
|
||||
NewPath(Arc<Path>),
|
||||
RemPath(Arc<Path>),
|
||||
}
|
||||
|
||||
struct InotifyWatcher {
|
||||
tx: Sender<WatcherStateUpdate>,
|
||||
}
|
||||
|
||||
struct PollWatcher {}
|
||||
|
||||
enum Never {}
|
||||
|
||||
impl LinuxWatcher {
|
||||
pub fn init(
|
||||
executor: BackgroundExecutor,
|
||||
) -> (Self, Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>) {
|
||||
let (tx_paths, rx_paths) = smol::channel::unbounded();
|
||||
let (tx_events, rx_events) = smol::channel::unbounded();
|
||||
|
||||
let task = executor.spawn::<Result<Never>>(async move {
|
||||
const WATCH_MASK: WatchMask = WatchMask::CREATE
|
||||
.union(WatchMask::CLOSE_WRITE)
|
||||
.union(WatchMask::DELETE)
|
||||
.union(WatchMask::DELETE_SELF)
|
||||
.union(WatchMask::MODIFY)
|
||||
.union(WatchMask::MOVE)
|
||||
.union(WatchMask::MOVE_SELF); // todo: fix this
|
||||
let mut buf = Box::<[u8; 1024]>::new_uninit();
|
||||
unsafe {
|
||||
buf.as_mut_ptr().write_bytes(0, 1);
|
||||
}
|
||||
let mut buf = unsafe { buf.assume_init() };
|
||||
let mut watcher = Inotify::init()?;
|
||||
let mut path_map = HashMap::default();
|
||||
loop {
|
||||
//dbg!();
|
||||
for upd in (0..).map_while(|_| rx_paths.try_recv().ok()) {
|
||||
//dbg!(&upd);
|
||||
match upd {
|
||||
WatcherStateUpdate::NewPath(p) => {
|
||||
//dbg!();
|
||||
let wd = watcher.watches().add(p.clone(), WATCH_MASK)?;
|
||||
//dbg!();
|
||||
path_map.insert(wd, p.clone());
|
||||
tx_events
|
||||
.send(vec![PathEvent {
|
||||
path: p.to_path_buf(),
|
||||
kind: None,
|
||||
}])
|
||||
.await?;
|
||||
//dbg!();
|
||||
log::info!("Watching {p:?}");
|
||||
}
|
||||
WatcherStateUpdate::RemPath(p) => {
|
||||
if let Some((wd, _)) = path_map.iter().find(|(_, v)| **v == p) {
|
||||
let wd = wd.clone();
|
||||
path_map.remove(&wd);
|
||||
watcher.watches().remove(wd)?;
|
||||
//dbg!();
|
||||
log::info!("No longer watching {p:?}");
|
||||
}
|
||||
tx_events
|
||||
.send(vec![PathEvent {
|
||||
path: p.to_path_buf(),
|
||||
kind: None,
|
||||
}])
|
||||
.await?;
|
||||
//dbg!();
|
||||
}
|
||||
};
|
||||
}
|
||||
//dbg!();
|
||||
let mut evts = vec![];
|
||||
// todo: skip wouldblock errors
|
||||
match watcher.read_events(&mut *buf) {
|
||||
Ok(raw_events) => {
|
||||
for evt in raw_events {
|
||||
log::info!("Got event {evt:?}");
|
||||
|
||||
let Some(path) = path_map.get(&evt.wd) else {
|
||||
continue;
|
||||
};
|
||||
let path = path.to_path_buf();
|
||||
if let Some(extra) = evt.name {
|
||||
evts.push(dbg!(PathEvent {
|
||||
path: path.join(extra),
|
||||
kind: None,
|
||||
}))
|
||||
//path = dbg!(path.join(extra));
|
||||
}
|
||||
evts.push(dbg!(PathEvent { path, kind: None }));
|
||||
}
|
||||
}
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::WouldBlock => {
|
||||
//dbg!()
|
||||
}
|
||||
_ => {
|
||||
util::log_err(&e);
|
||||
//dbg!(&e);
|
||||
//return Err(e.into());
|
||||
}
|
||||
},
|
||||
}
|
||||
if !evts.is_empty() {
|
||||
tx_events.send(evts).await?;
|
||||
}
|
||||
//dbg!();
|
||||
std::thread::yield_now();
|
||||
}
|
||||
});
|
||||
|
||||
task.detach();
|
||||
(
|
||||
Self {
|
||||
inotify: InotifyWatcher { tx: tx_paths },
|
||||
//task,
|
||||
poller: PollWatcher {},
|
||||
},
|
||||
Box::pin(rx_events),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher for LinuxWatcher {
|
||||
fn add(&self, path: &std::path::Path) -> Result<()> {
|
||||
block_on(async {
|
||||
self.inotify
|
||||
.tx
|
||||
.send(WatcherStateUpdate::NewPath(path.into()))
|
||||
.await
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, path: &std::path::Path) -> Result<()> {
|
||||
block_on(async {
|
||||
self.inotify
|
||||
.tx
|
||||
.send(WatcherStateUpdate::RemPath(path.into()))
|
||||
.await
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// For some reason, tests are currently broken. Don't reenable until this is figured
|
||||
// out.
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use std::{
|
||||
// path::PathBuf,
|
||||
// time::{Duration, Instant},
|
||||
// };
|
||||
|
||||
// use collections::HashSet;
|
||||
// use gpui::TestAppContext;
|
||||
// use smol::stream::StreamExt;
|
||||
// use tempfile::TempDir;
|
||||
|
||||
// use crate::{Fs, RealFs};
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn randomized_file_watcher_tests(cx: &mut TestAppContext) {
|
||||
// let dir = TempDir::new().unwrap();
|
||||
// let fs = RealFs::new(None, cx.executor());
|
||||
// let (mut events, watcher) = fs.watch(dir.path(), Duration::from_millis(100)).await;
|
||||
|
||||
// dbg!("gonna create dir");
|
||||
// std::fs::create_dir(dir.path().join("test")).unwrap();
|
||||
// dbg!("created dir");
|
||||
// let expected_paths: HashSet<PathBuf> = [dir.path().to_owned()].into_iter().collect();
|
||||
// let mut actual_paths = HashSet::default();
|
||||
|
||||
// let start = Instant::now();
|
||||
// while start.elapsed() < Duration::from_millis(1000) {
|
||||
// dbg!();
|
||||
// //std::thread::sleep(Duration::from_secs(1));
|
||||
// let events = events.next().await;
|
||||
// dbg!();
|
||||
// for event in events.into_iter().flatten() {
|
||||
// actual_paths.insert(dbg!(event.path));
|
||||
// }
|
||||
// dbg!();
|
||||
// //std::fs::write(dir.path().join("filename"), [42]).unwrap();
|
||||
// }
|
||||
|
||||
// assert_eq!(expected_paths, actual_paths);
|
||||
|
||||
// drop(watcher)
|
||||
// }
|
||||
// }
|
||||
@@ -703,7 +703,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
fs::fs_watcher::global(|_| {}).unwrap();
|
||||
|
||||
cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
|
||||
|
||||
@@ -379,7 +379,7 @@ pub fn initialize_workspace(
|
||||
})
|
||||
.detach();
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
initialize_file_watcher(window, cx);
|
||||
|
||||
if let Some(specs) = window.gpu_specs() {
|
||||
@@ -525,7 +525,8 @@ fn unstable_version_notification(cx: &mut App) {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
// todo!()
|
||||
#[cfg(any(target_os = "freebsd"))]
|
||||
fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
if let Err(e) = fs::fs_watcher::global(|_| {}) {
|
||||
let message = format!(
|
||||
|
||||
Reference in New Issue
Block a user