Add rs-notify implementation of fs::watch (#9040)

This PR simplifies the Zed file system abstraction and implements
`Fs::watch` for linux and windows.

TODO:
- [x] Figure out why this fails to initialize the file watchers when we
have to initialize the config directory paths, but succeeds on
subsequent runs.
- [x] Fix macOS dependencies on old fsevents::Event crate

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2024-03-08 22:18:44 -08:00 committed by GitHub
parent 456efb53ad
commit ca696fd5f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 478 additions and 493 deletions

1
Cargo.lock generated
View File

@ -12790,7 +12790,6 @@ dependencies = [
"feedback", "feedback",
"file_finder", "file_finder",
"fs", "fs",
"fsevent",
"futures 0.3.28", "futures 0.3.28",
"go_to_line", "go_to_line",
"gpui", "gpui",

View File

@ -296,10 +296,10 @@ impl ExtensionStore {
let reload_tx = this.reload_tx.clone(); let reload_tx = this.reload_tx.clone();
let installed_dir = this.installed_dir.clone(); let installed_dir = this.installed_dir.clone();
async move { async move {
let mut events = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
while let Some(events) = events.next().await { while let Some(paths) = paths.next().await {
for event in events { for path in paths {
let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { let Ok(event_path) = path.strip_prefix(&installed_dir) else {
continue; continue;
}; };

View File

@ -13,7 +13,6 @@ path = "src/fs.rs"
[dependencies] [dependencies]
collections.workspace = true collections.workspace = true
fsevent.workspace = true
rope.workspace = true rope.workspace = true
text.workspace = true text.workspace = true
util.workspace = true util.workspace = true
@ -37,6 +36,9 @@ time.workspace = true
gpui = { workspace = true, optional = true } gpui = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
fsevent.workspace = true
[target.'cfg(not(target_os = "macos"))'.dependencies] [target.'cfg(not(target_os = "macos"))'.dependencies]
notify = "6.1.1" notify = "6.1.1"

View File

@ -1,15 +1,6 @@
pub mod repository; pub mod repository;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
pub use fsevent::Event;
#[cfg(target_os = "macos")]
use fsevent::EventStream;
#[cfg(not(target_os = "macos"))]
use fsevent::StreamFlags;
#[cfg(not(target_os = "macos"))]
use notify::{Config, EventKind, Watcher};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
@ -76,7 +67,7 @@ pub trait Fs: Send + Sync {
&self, &self,
path: &Path, path: &Path,
latency: Duration, latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>>; ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
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;
@ -327,12 +318,18 @@ impl Fs for RealFs {
&self, &self,
path: &Path, path: &Path,
latency: Duration, latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> { ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
use fsevent::EventStream;
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 || {
stream.run(move |events| smol::block_on(tx.send(events)).is_ok()); stream.run(move |events| {
smol::block_on(tx.send(events.into_iter().map(|event| event.path).collect()))
.is_ok()
});
}); });
Box::pin(rx.chain(futures::stream::once(async move { Box::pin(rx.chain(futures::stream::once(async move {
drop(handle); drop(handle);
vec![] vec![]
@ -343,49 +340,66 @@ impl Fs for RealFs {
async fn watch( async fn watch(
&self, &self,
path: &Path, path: &Path,
latency: Duration, _latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> { ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
use notify::{event::EventKind, Watcher};
// todo(linux): This spawns two threads, while the macOS impl
// only spawns one. Can we use a OnceLock or some such to make
// this better
let (tx, rx) = smol::channel::unbounded(); let (tx, rx) = smol::channel::unbounded();
if !path.exists() { let mut file_watcher = notify::recommended_watcher({
log::error!("watch path does not exist: {}", path.display()); let tx = tx.clone();
return Box::pin(rx); move |event: Result<notify::Event, _>| {
} if let Some(event) = event.log_err() {
tx.try_send(event.paths).ok();
let mut watcher =
notify::recommended_watcher(move |res: Result<notify::Event, _>| match res {
Ok(event) => {
let flags = match event.kind {
// ITEM_REMOVED is currently the only flag we care about
EventKind::Remove(_) => StreamFlags::ITEM_REMOVED,
_ => StreamFlags::NONE,
};
let events = event
.paths
.into_iter()
.map(|path| Event {
event_id: 0,
flags,
path,
})
.collect::<Vec<_>>();
let _ = tx.try_send(events);
} }
Err(err) => { }
log::error!("watch error: {}", err); })
} .expect("Could not start file watcher");
})
.unwrap();
watcher file_watcher
.configure(Config::default().with_poll_interval(latency))
.unwrap();
watcher
.watch(path, notify::RecursiveMode::Recursive) .watch(path, notify::RecursiveMode::Recursive)
.unwrap(); .ok(); // It's ok if this fails, the parent watcher will add it.
Box::pin(rx) let mut parent_watcher = notify::recommended_watcher({
let watched_path = path.to_path_buf();
let tx = tx.clone();
move |event: Result<notify::Event, _>| {
if let Some(event) = event.ok() {
if event.paths.into_iter().any(|path| *path == watched_path) {
match event.kind {
EventKind::Create(_) => {
file_watcher
.watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
.log_err();
let _ = tx.try_send(vec![watched_path.clone()]).ok();
}
EventKind::Remove(_) => {
file_watcher.unwatch(&watched_path).log_err();
let _ = tx.try_send(vec![watched_path.clone()]).ok();
}
_ => {}
}
}
}
}
})
.expect("Could not start file watcher");
parent_watcher
.watch(
path.parent()
.expect("Watching root is probably not what you want"),
notify::RecursiveMode::NonRecursive,
)
.expect("Could not start watcher on parent directory");
Box::pin(rx.chain(futures::stream::once(async move {
drop(parent_watcher);
vec![]
})))
} }
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> { fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
@ -443,10 +457,6 @@ impl Fs for RealFs {
} }
} }
pub fn fs_events_paths(events: Vec<Event>) -> Vec<PathBuf> {
events.into_iter().map(|event| event.path).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.
@ -459,9 +469,9 @@ struct FakeFsState {
root: Arc<Mutex<FakeFsEntry>>, root: Arc<Mutex<FakeFsEntry>>,
next_inode: u64, next_inode: u64,
next_mtime: SystemTime, next_mtime: SystemTime,
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>, event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
events_paused: bool, events_paused: bool,
buffered_events: Vec<fsevent::Event>, buffered_events: Vec<PathBuf>,
metadata_call_count: usize, metadata_call_count: usize,
read_dir_call_count: usize, read_dir_call_count: usize,
} }
@ -569,11 +579,7 @@ impl FakeFsState {
T: Into<PathBuf>, T: Into<PathBuf>,
{ {
self.buffered_events self.buffered_events
.extend(paths.into_iter().map(|path| fsevent::Event { .extend(paths.into_iter().map(Into::into));
event_id: 0,
flags: fsevent::StreamFlags::empty(),
path: path.into(),
}));
if !self.events_paused { if !self.events_paused {
self.flush_events(self.buffered_events.len()); self.flush_events(self.buffered_events.len());
@ -1328,14 +1334,14 @@ impl Fs for FakeFs {
&self, &self,
path: &Path, path: &Path,
_: Duration, _: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> { ) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
self.simulate_random_delay().await; self.simulate_random_delay().await;
let (tx, rx) = smol::channel::unbounded(); let (tx, rx) = smol::channel::unbounded();
self.state.lock().event_txs.push(tx); self.state.lock().event_txs.push(tx);
let path = path.to_path_buf(); let path = path.to_path_buf();
let executor = self.executor.clone(); let executor = self.executor.clone();
Box::pin(futures::StreamExt::filter(rx, move |events| { Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events.iter().any(|event| event.path.starts_with(&path)); let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
let executor = executor.clone(); let executor = executor.clone();
async move { async move {
executor.simulate_random_delay().await; executor.simulate_random_delay().await;

View File

@ -1,11 +1,17 @@
#[cfg(target_os = "macos")] #![cfg(target_os = "macos")]
pub use mac_impl::*;
use bitflags::bitflags; use bitflags::bitflags;
use std::path::PathBuf; use fsevent_sys::{self as fs, core_foundation as cf};
use parking_lot::Mutex;
#[cfg(target_os = "macos")] use std::{
mod mac_impl; convert::AsRef,
ffi::{c_void, CStr, OsStr},
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
ptr, slice,
sync::Arc,
time::Duration,
};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Event { pub struct Event {
@ -14,10 +20,244 @@ pub struct Event {
pub path: PathBuf, pub path: PathBuf,
} }
pub struct EventStream {
lifecycle: Arc<Mutex<Lifecycle>>,
state: Box<State>,
}
struct State {
latency: Duration,
paths: cf::CFMutableArrayRef,
callback: Option<Box<dyn FnMut(Vec<Event>) -> bool>>,
last_valid_event_id: Option<fs::FSEventStreamEventId>,
stream: fs::FSEventStreamRef,
}
impl Drop for State {
fn drop(&mut self) {
unsafe {
cf::CFRelease(self.paths);
fs::FSEventStreamStop(self.stream);
fs::FSEventStreamInvalidate(self.stream);
fs::FSEventStreamRelease(self.stream);
}
}
}
enum Lifecycle {
New,
Running(cf::CFRunLoopRef),
Stopped,
}
pub struct Handle(Arc<Mutex<Lifecycle>>);
unsafe impl Send for EventStream {}
unsafe impl Send for Lifecycle {}
impl EventStream {
pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
unsafe {
let cf_paths =
cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
assert!(!cf_paths.is_null());
for path in paths {
let path_bytes = path.as_os_str().as_bytes();
let cf_url = cf::CFURLCreateFromFileSystemRepresentation(
cf::kCFAllocatorDefault,
path_bytes.as_ptr() as *const i8,
path_bytes.len() as cf::CFIndex,
false,
);
let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle);
cf::CFArrayAppendValue(cf_paths, cf_path);
cf::CFRelease(cf_path);
cf::CFRelease(cf_url);
}
let mut state = Box::new(State {
latency,
paths: cf_paths,
callback: None,
last_valid_event_id: None,
stream: ptr::null_mut(),
});
let stream_context = fs::FSEventStreamContext {
version: 0,
info: state.as_ref() as *const _ as *mut c_void,
retain: None,
release: None,
copy_description: None,
};
let stream = fs::FSEventStreamCreate(
cf::kCFAllocatorDefault,
Self::trampoline,
&stream_context,
cf_paths,
FSEventsGetCurrentEventId(),
latency.as_secs_f64(),
fs::kFSEventStreamCreateFlagFileEvents
| fs::kFSEventStreamCreateFlagNoDefer
| fs::kFSEventStreamCreateFlagWatchRoot,
);
state.stream = stream;
let lifecycle = Arc::new(Mutex::new(Lifecycle::New));
(
EventStream {
lifecycle: lifecycle.clone(),
state,
},
Handle(lifecycle),
)
}
}
pub fn run<F>(mut self, f: F)
where
F: FnMut(Vec<Event>) -> bool + 'static,
{
self.state.callback = Some(Box::new(f));
unsafe {
let run_loop = cf::CFRunLoopGetCurrent();
{
let mut state = self.lifecycle.lock();
match *state {
Lifecycle::New => *state = Lifecycle::Running(run_loop),
Lifecycle::Running(_) => unreachable!(),
Lifecycle::Stopped => return,
}
}
fs::FSEventStreamScheduleWithRunLoop(
self.state.stream,
run_loop,
cf::kCFRunLoopDefaultMode,
);
fs::FSEventStreamStart(self.state.stream);
cf::CFRunLoopRun();
}
}
extern "C" fn trampoline(
stream_ref: fs::FSEventStreamRef,
info: *mut ::std::os::raw::c_void,
num: usize, // size_t numEvents
event_paths: *mut ::std::os::raw::c_void, // void *eventPaths
event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[]
event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[]
) {
unsafe {
let event_paths = event_paths as *const *const ::std::os::raw::c_char;
let e_ptr = event_flags as *mut u32;
let i_ptr = event_ids as *mut u64;
let state = (info as *mut State).as_mut().unwrap();
let callback = if let Some(callback) = state.callback.as_mut() {
callback
} else {
return;
};
let paths = slice::from_raw_parts(event_paths, num);
let flags = slice::from_raw_parts_mut(e_ptr, num);
let ids = slice::from_raw_parts_mut(i_ptr, num);
let mut stream_restarted = false;
// Sometimes FSEvents reports a "dropped" event, an indication that either the kernel
// or our code couldn't keep up with the sheer volume of file-system events that were
// generated. If we observed a valid event before this happens, we'll try to read the
// file-system journal by stopping the current stream and creating a new one starting at
// such event. Otherwise, we'll let invoke the callback with the dropped event, which
// will likely perform a re-scan of one of the root directories.
if flags
.iter()
.copied()
.filter_map(StreamFlags::from_bits)
.any(|flags| {
flags.contains(StreamFlags::USER_DROPPED)
|| flags.contains(StreamFlags::KERNEL_DROPPED)
})
{
if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
fs::FSEventStreamStop(state.stream);
fs::FSEventStreamInvalidate(state.stream);
fs::FSEventStreamRelease(state.stream);
let stream_context = fs::FSEventStreamContext {
version: 0,
info,
retain: None,
release: None,
copy_description: None,
};
let stream = fs::FSEventStreamCreate(
cf::kCFAllocatorDefault,
Self::trampoline,
&stream_context,
state.paths,
last_valid_event_id,
state.latency.as_secs_f64(),
fs::kFSEventStreamCreateFlagFileEvents
| fs::kFSEventStreamCreateFlagNoDefer
| fs::kFSEventStreamCreateFlagWatchRoot,
);
state.stream = stream;
fs::FSEventStreamScheduleWithRunLoop(
state.stream,
cf::CFRunLoopGetCurrent(),
cf::kCFRunLoopDefaultMode,
);
fs::FSEventStreamStart(state.stream);
stream_restarted = true;
}
}
if !stream_restarted {
let mut events = Vec::with_capacity(num);
for p in 0..num {
if let Some(flag) = StreamFlags::from_bits(flags[p]) {
if !flag.contains(StreamFlags::HISTORY_DONE) {
let path_c_str = CStr::from_ptr(paths[p]);
let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
let event = Event {
event_id: ids[p],
flags: flag,
path,
};
state.last_valid_event_id = Some(event.event_id);
events.push(event);
}
} else {
debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
}
}
if !events.is_empty() && !callback(events) {
fs::FSEventStreamStop(stream_ref);
cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
}
}
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
let mut state = self.0.lock();
if let Lifecycle::Running(run_loop) = *state {
unsafe {
cf::CFRunLoopStop(run_loop);
}
}
*state = Lifecycle::Stopped;
}
}
// Synchronize with // Synchronize with
// /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h // /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h
bitflags! { bitflags! {
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[repr(C)] #[repr(C)]
pub struct StreamFlags: u32 { pub struct StreamFlags: u32 {
const NONE = 0x00000000; const NONE = 0x00000000;
@ -121,3 +361,138 @@ impl std::fmt::Display for StreamFlags {
write!(f, "") write!(f, "")
} }
} }
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
pub fn FSEventsGetCurrentEventId() -> u64;
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, sync::mpsc, thread, time::Duration};
#[test]
fn test_event_stream_simple() {
for _ in 0..3 {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_delayed_start() {
for _ in 0..3 {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
// Delay the call to `run` in order to make sure we don't miss any events that occur
// between creating the `EventStream` and calling `run`.
thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
stream.run(move |events| tx.send(events.to_vec()).is_ok())
});
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_shutdown_by_dropping_handle() {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || {
stream.run({
let tx = tx.clone();
move |_| {
tx.send("running").unwrap();
true
}
});
tx.send("stopped").unwrap();
});
fs::write(path.join("new-file"), "").unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
// Dropping the handle causes `EventStream::run` to return.
drop(handle);
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
}
#[test]
fn test_event_stream_shutdown_before_run() {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
drop(handle);
// This returns immediately because the handle was already dropped.
stream.run(|_| true);
}
fn flush_historical_events() {
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
};
thread::sleep(duration);
}
}

View File

@ -1,382 +0,0 @@
use fsevent_sys::{self as fs, core_foundation as cf};
use parking_lot::Mutex;
use std::{
convert::AsRef,
ffi::{c_void, CStr, OsStr},
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
ptr, slice,
sync::Arc,
time::Duration,
};
use crate::{Event, StreamFlags};
pub struct EventStream {
lifecycle: Arc<Mutex<Lifecycle>>,
state: Box<State>,
}
struct State {
latency: Duration,
paths: cf::CFMutableArrayRef,
callback: Option<Box<dyn FnMut(Vec<Event>) -> bool>>,
last_valid_event_id: Option<fs::FSEventStreamEventId>,
stream: fs::FSEventStreamRef,
}
impl Drop for State {
fn drop(&mut self) {
unsafe {
cf::CFRelease(self.paths);
fs::FSEventStreamStop(self.stream);
fs::FSEventStreamInvalidate(self.stream);
fs::FSEventStreamRelease(self.stream);
}
}
}
enum Lifecycle {
New,
Running(cf::CFRunLoopRef),
Stopped,
}
pub struct Handle(Arc<Mutex<Lifecycle>>);
unsafe impl Send for EventStream {}
unsafe impl Send for Lifecycle {}
impl EventStream {
pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
unsafe {
let cf_paths =
cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
assert!(!cf_paths.is_null());
for path in paths {
let path_bytes = path.as_os_str().as_bytes();
let cf_url = cf::CFURLCreateFromFileSystemRepresentation(
cf::kCFAllocatorDefault,
path_bytes.as_ptr() as *const i8,
path_bytes.len() as cf::CFIndex,
false,
);
let cf_path = cf::CFURLCopyFileSystemPath(cf_url, cf::kCFURLPOSIXPathStyle);
cf::CFArrayAppendValue(cf_paths, cf_path);
cf::CFRelease(cf_path);
cf::CFRelease(cf_url);
}
let mut state = Box::new(State {
latency,
paths: cf_paths,
callback: None,
last_valid_event_id: None,
stream: ptr::null_mut(),
});
let stream_context = fs::FSEventStreamContext {
version: 0,
info: state.as_ref() as *const _ as *mut c_void,
retain: None,
release: None,
copy_description: None,
};
let stream = fs::FSEventStreamCreate(
cf::kCFAllocatorDefault,
Self::trampoline,
&stream_context,
cf_paths,
FSEventsGetCurrentEventId(),
latency.as_secs_f64(),
fs::kFSEventStreamCreateFlagFileEvents
| fs::kFSEventStreamCreateFlagNoDefer
| fs::kFSEventStreamCreateFlagWatchRoot,
);
state.stream = stream;
let lifecycle = Arc::new(Mutex::new(Lifecycle::New));
(
EventStream {
lifecycle: lifecycle.clone(),
state,
},
Handle(lifecycle),
)
}
}
pub fn run<F>(mut self, f: F)
where
F: FnMut(Vec<Event>) -> bool + 'static,
{
self.state.callback = Some(Box::new(f));
unsafe {
let run_loop = cf::CFRunLoopGetCurrent();
{
let mut state = self.lifecycle.lock();
match *state {
Lifecycle::New => *state = Lifecycle::Running(run_loop),
Lifecycle::Running(_) => unreachable!(),
Lifecycle::Stopped => return,
}
}
fs::FSEventStreamScheduleWithRunLoop(
self.state.stream,
run_loop,
cf::kCFRunLoopDefaultMode,
);
fs::FSEventStreamStart(self.state.stream);
cf::CFRunLoopRun();
}
}
extern "C" fn trampoline(
stream_ref: fs::FSEventStreamRef,
info: *mut ::std::os::raw::c_void,
num: usize, // size_t numEvents
event_paths: *mut ::std::os::raw::c_void, // void *eventPaths
event_flags: *const ::std::os::raw::c_void, // const FSEventStreamEventFlags eventFlags[]
event_ids: *const ::std::os::raw::c_void, // const FSEventStreamEventId eventIds[]
) {
unsafe {
let event_paths = event_paths as *const *const ::std::os::raw::c_char;
let e_ptr = event_flags as *mut u32;
let i_ptr = event_ids as *mut u64;
let state = (info as *mut State).as_mut().unwrap();
let callback = if let Some(callback) = state.callback.as_mut() {
callback
} else {
return;
};
let paths = slice::from_raw_parts(event_paths, num);
let flags = slice::from_raw_parts_mut(e_ptr, num);
let ids = slice::from_raw_parts_mut(i_ptr, num);
let mut stream_restarted = false;
// Sometimes FSEvents reports a "dropped" event, an indication that either the kernel
// or our code couldn't keep up with the sheer volume of file-system events that were
// generated. If we observed a valid event before this happens, we'll try to read the
// file-system journal by stopping the current stream and creating a new one starting at
// such event. Otherwise, we'll let invoke the callback with the dropped event, which
// will likely perform a re-scan of one of the root directories.
if flags
.iter()
.copied()
.filter_map(StreamFlags::from_bits)
.any(|flags| {
flags.contains(StreamFlags::USER_DROPPED)
|| flags.contains(StreamFlags::KERNEL_DROPPED)
})
{
if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
fs::FSEventStreamStop(state.stream);
fs::FSEventStreamInvalidate(state.stream);
fs::FSEventStreamRelease(state.stream);
let stream_context = fs::FSEventStreamContext {
version: 0,
info,
retain: None,
release: None,
copy_description: None,
};
let stream = fs::FSEventStreamCreate(
cf::kCFAllocatorDefault,
Self::trampoline,
&stream_context,
state.paths,
last_valid_event_id,
state.latency.as_secs_f64(),
fs::kFSEventStreamCreateFlagFileEvents
| fs::kFSEventStreamCreateFlagNoDefer
| fs::kFSEventStreamCreateFlagWatchRoot,
);
state.stream = stream;
fs::FSEventStreamScheduleWithRunLoop(
state.stream,
cf::CFRunLoopGetCurrent(),
cf::kCFRunLoopDefaultMode,
);
fs::FSEventStreamStart(state.stream);
stream_restarted = true;
}
}
if !stream_restarted {
let mut events = Vec::with_capacity(num);
for p in 0..num {
if let Some(flag) = StreamFlags::from_bits(flags[p]) {
if !flag.contains(StreamFlags::HISTORY_DONE) {
let path_c_str = CStr::from_ptr(paths[p]);
let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
let event = Event {
event_id: ids[p],
flags: flag,
path,
};
state.last_valid_event_id = Some(event.event_id);
events.push(event);
}
} else {
debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
}
}
if !events.is_empty() && !callback(events) {
fs::FSEventStreamStop(stream_ref);
cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
}
}
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
let mut state = self.0.lock();
if let Lifecycle::Running(run_loop) = *state {
unsafe {
cf::CFRunLoopStop(run_loop);
}
}
*state = Lifecycle::Stopped;
}
}
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
pub fn FSEventsGetCurrentEventId() -> u64;
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, sync::mpsc, thread, time::Duration};
#[test]
fn test_event_stream_simple() {
for _ in 0..3 {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_delayed_start() {
for _ in 0..3 {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
for i in 0..10 {
fs::write(path.join(format!("existing-file-{}", i)), "").unwrap();
}
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
// Delay the call to `run` in order to make sure we don't miss any events that occur
// between creating the `EventStream` and calling `run`.
thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
stream.run(move |events| tx.send(events.to_vec()).is_ok())
});
fs::write(path.join("new-file"), "").unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("new-file"));
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
fs::remove_file(path.join("existing-file-5")).unwrap();
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
let event = events.last().unwrap();
assert_eq!(event.path, path.join("existing-file-5"));
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
drop(handle);
}
}
#[test]
fn test_event_stream_shutdown_by_dropping_handle() {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
flush_historical_events();
let (tx, rx) = mpsc::channel();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
thread::spawn(move || {
stream.run({
let tx = tx.clone();
move |_| {
tx.send("running").unwrap();
true
}
});
tx.send("stopped").unwrap();
});
fs::write(path.join("new-file"), "").unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
// Dropping the handle causes `EventStream::run` to return.
drop(handle);
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
}
#[test]
fn test_event_stream_shutdown_before_run() {
let dir = tempfile::Builder::new()
.prefix("test-event-stream")
.tempdir()
.unwrap();
let path = dir.path().canonicalize().unwrap();
let (stream, handle) = EventStream::new(&[&path], Duration::from_millis(50));
drop(handle);
// This returns immediately because the handle was already dropped.
stream.run(|_| true);
}
fn flush_historical_events() {
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
};
thread::sleep(duration);
}
}

View File

@ -324,7 +324,7 @@ impl Platform for LinuxPlatform {
}) })
} }
//TODO linux //todo(linux)
fn app_path(&self) -> Result<PathBuf> { fn app_path(&self) -> Result<PathBuf> {
Err(anyhow::Error::msg( Err(anyhow::Error::msg(
"Platform<LinuxPlatform>::app_path is not implemented yet", "Platform<LinuxPlatform>::app_path is not implemented yet",
@ -338,7 +338,7 @@ impl Platform for LinuxPlatform {
UtcOffset::UTC UtcOffset::UTC
} }
//TODO linux //todo(linux)
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> { fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
Err(anyhow::Error::msg( Err(anyhow::Error::msg(
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet", "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
@ -390,8 +390,7 @@ impl Platform for LinuxPlatform {
}) })
} }
//TODO linux: add trait methods for accessing the primary selection //todo(linux): add trait methods for accessing the primary selection
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> { fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
let url = url.to_string(); let url = url.to_string();
self.background_executor().spawn(async move { self.background_executor().spawn(async move {

View File

@ -281,7 +281,7 @@ impl Client for WaylandClient {
CursorStyle::ResizeUp => "n-resize".to_string(), CursorStyle::ResizeUp => "n-resize".to_string(),
CursorStyle::ResizeDown => "s-resize".to_string(), CursorStyle::ResizeDown => "s-resize".to_string(),
CursorStyle::ResizeUpDown => "ns-resize".to_string(), CursorStyle::ResizeUpDown => "ns-resize".to_string(),
CursorStyle::DisappearingItem => "grabbing".to_string(), // TODO linux - couldn't find equivalent icon in linux CursorStyle::DisappearingItem => "grabbing".to_string(), // todo(linux) - couldn't find equivalent icon in linux
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text".to_string(), CursorStyle::IBeamCursorForVerticalLayout => "vertical-text".to_string(),
CursorStyle::OperationNotAllowed => "not-allowed".to_string(), CursorStyle::OperationNotAllowed => "not-allowed".to_string(),
CursorStyle::DragLink => "dnd-link".to_string(), CursorStyle::DragLink => "dnd-link".to_string(),

View File

@ -361,7 +361,7 @@ impl Client for X11Client {
Box::new(X11Window(window_ptr)) Box::new(X11Window(window_ptr))
} }
// TODO linux //todo(linux)
fn set_cursor_style(&self, _style: CursorStyle) {} fn set_cursor_style(&self, _style: CursorStyle) {}
fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> { fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {

View File

@ -3336,7 +3336,7 @@ impl BackgroundScanner {
} }
} }
async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<fs::Event>>>>) { async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>) {
use futures::FutureExt as _; use futures::FutureExt as _;
// Populate ignores above the root. // Populate ignores above the root.
@ -3389,11 +3389,9 @@ impl BackgroundScanner {
// For these events, update events cannot be as precise, because we didn't // For these events, update events cannot be as precise, because we didn't
// 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(mut paths)) = futures::poll!(fs_events_rx.next()) {
let mut paths = fs::fs_events_paths(events); while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
paths.extend(more_paths);
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
paths.extend(fs::fs_events_paths(more_events));
} }
self.process_events(paths).await; self.process_events(paths).await;
} }
@ -3430,12 +3428,10 @@ impl BackgroundScanner {
} }
} }
events = fs_events_rx.next().fuse() => { paths = fs_events_rx.next().fuse() => {
let Some(events) = events else { break }; let Some(mut paths) = paths else { break };
let mut paths = fs::fs_events_paths(events); while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
paths.extend(more_paths);
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
paths.extend(fs::fs_events_paths(more_events));
} }
self.process_events(paths.clone()).await; self.process_events(paths.clone()).await;
} }

View File

@ -48,7 +48,6 @@ extensions_ui.workspace = true
feedback.workspace = true feedback.workspace = true
file_finder.workspace = true file_finder.workspace = true
fs.workspace = true fs.workspace = true
fsevent.workspace = true
futures.workspace = true futures.workspace = true
go_to_line.workspace = true go_to_line.workspace = true
gpui.workspace = true gpui.workspace = true

View File

@ -11,8 +11,6 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use env_logger::Builder; use env_logger::Builder;
use fs::RealFs; use fs::RealFs;
#[cfg(target_os = "macos")]
use fsevent::StreamFlags;
use futures::{future, StreamExt}; use futures::{future, StreamExt};
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
use isahc::{prelude::Configurable, Request}; use isahc::{prelude::Configurable, Request};
@ -184,7 +182,6 @@ fn main() {
); );
load_user_themes_in_background(fs.clone(), cx); load_user_themes_in_background(fs.clone(), cx);
#[cfg(target_os = "macos")]
watch_themes(fs.clone(), cx); watch_themes(fs.clone(), cx);
cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
@ -465,10 +462,11 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: AsyncAppConte
fn init_paths() { fn init_paths() {
std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path"); std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
std::fs::create_dir_all(&*util::paths::EXTENSIONS_DIR)
.expect("could not create extensions path");
std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path"); std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path"); std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path"); std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
#[cfg(target_os = "linux")]
std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path"); std::fs::create_dir_all(&*util::paths::TEMP_DIR).expect("could not create tmp path");
} }
@ -974,9 +972,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
// todo(linux): Port fsevents to linux
/// Spawns a background task to watch the themes directory for changes. /// Spawns a background task to watch the themes directory for changes.
#[cfg(target_os = "macos")]
fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) { fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
use std::time::Duration; use std::time::Duration;
cx.spawn(|cx| async move { cx.spawn(|cx| async move {
@ -984,17 +980,14 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
.watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100)) .watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100))
.await; .await;
while let Some(events) = events.next().await { while let Some(paths) = events.next().await {
for event in events { for path in paths {
if event.flags.contains(StreamFlags::ITEM_REMOVED) { if fs.metadata(&path).await.ok().flatten().is_some() {
// Theme was removed, don't need to reload.
// We may want to remove the theme from the registry, in this case.
} else {
if let Some(theme_registry) = if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{ {
if let Some(()) = theme_registry if let Some(()) = theme_registry
.load_user_theme(&event.path, fs.clone()) .load_user_theme(&path, fs.clone())
.await .await
.log_err() .log_err()
{ {

View File

@ -47,10 +47,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
clippy_command.arg("--workspace"); clippy_command.arg("--workspace");
} }
clippy_command clippy_command.arg("--release").arg("--all-features");
.arg("--release")
.arg("--all-targets")
.arg("--all-features");
if args.fix { if args.fix {
clippy_command.arg("--fix"); clippy_command.arg("--fix");
@ -60,6 +57,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
// Deny all warnings. // Deny all warnings.
// We don't do this yet on Windows, as it still has some warnings present. // We don't do this yet on Windows, as it still has some warnings present.
// todo(windows)
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
clippy_command.args(["--deny", "warnings"]); clippy_command.args(["--deny", "warnings"]);