mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
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:
parent
456efb53ad
commit
ca696fd5f6
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.expect("Could not start file watcher");
|
||||||
|
|
||||||
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;
|
||||||
|
@ -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,6 +20,240 @@ 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! {
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
|
@ -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>> {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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"]);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user