mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00: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
13 changed files with 478 additions and 493 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -12790,7 +12790,6 @@ dependencies = [
|
|||
"feedback",
|
||||
"file_finder",
|
||||
"fs",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
"go_to_line",
|
||||
"gpui",
|
||||
|
|
|
@ -296,10 +296,10 @@ impl ExtensionStore {
|
|||
let reload_tx = this.reload_tx.clone();
|
||||
let installed_dir = this.installed_dir.clone();
|
||||
async move {
|
||||
let mut events = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
while let Some(events) = events.next().await {
|
||||
for event in events {
|
||||
let Ok(event_path) = event.path.strip_prefix(&installed_dir) else {
|
||||
let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
while let Some(paths) = paths.next().await {
|
||||
for path in paths {
|
||||
let Ok(event_path) = path.strip_prefix(&installed_dir) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ path = "src/fs.rs"
|
|||
|
||||
[dependencies]
|
||||
collections.workspace = true
|
||||
fsevent.workspace = true
|
||||
rope.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
|
@ -37,6 +36,9 @@ time.workspace = true
|
|||
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
fsevent.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
pub mod repository;
|
||||
|
||||
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)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
@ -76,7 +67,7 @@ pub trait Fs: Send + Sync {
|
|||
&self,
|
||||
path: &Path,
|
||||
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 is_fake(&self) -> bool;
|
||||
|
@ -327,12 +318,18 @@ impl Fs for RealFs {
|
|||
&self,
|
||||
path: &Path,
|
||||
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 (stream, handle) = EventStream::new(&[path], latency);
|
||||
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 {
|
||||
drop(handle);
|
||||
vec![]
|
||||
|
@ -343,49 +340,66 @@ impl Fs for RealFs {
|
|||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<Event>>>> {
|
||||
_latency: Duration,
|
||||
) -> 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();
|
||||
|
||||
if !path.exists() {
|
||||
log::error!("watch path does not exist: {}", path.display());
|
||||
return Box::pin(rx);
|
||||
}
|
||||
|
||||
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);
|
||||
let mut file_watcher = notify::recommended_watcher({
|
||||
let tx = tx.clone();
|
||||
move |event: Result<notify::Event, _>| {
|
||||
if let Some(event) = event.log_err() {
|
||||
tx.try_send(event.paths).ok();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("watch error: {}", err);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.expect("Could not start file watcher");
|
||||
|
||||
watcher
|
||||
.configure(Config::default().with_poll_interval(latency))
|
||||
.unwrap();
|
||||
|
||||
watcher
|
||||
file_watcher
|
||||
.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>>> {
|
||||
|
@ -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"))]
|
||||
pub struct FakeFs {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
|
@ -459,9 +469,9 @@ struct FakeFsState {
|
|||
root: Arc<Mutex<FakeFsEntry>>,
|
||||
next_inode: u64,
|
||||
next_mtime: SystemTime,
|
||||
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
|
||||
event_txs: Vec<smol::channel::Sender<Vec<PathBuf>>>,
|
||||
events_paused: bool,
|
||||
buffered_events: Vec<fsevent::Event>,
|
||||
buffered_events: Vec<PathBuf>,
|
||||
metadata_call_count: usize,
|
||||
read_dir_call_count: usize,
|
||||
}
|
||||
|
@ -569,11 +579,7 @@ impl FakeFsState {
|
|||
T: Into<PathBuf>,
|
||||
{
|
||||
self.buffered_events
|
||||
.extend(paths.into_iter().map(|path| fsevent::Event {
|
||||
event_id: 0,
|
||||
flags: fsevent::StreamFlags::empty(),
|
||||
path: path.into(),
|
||||
}));
|
||||
.extend(paths.into_iter().map(Into::into));
|
||||
|
||||
if !self.events_paused {
|
||||
self.flush_events(self.buffered_events.len());
|
||||
|
@ -1328,14 +1334,14 @@ impl Fs for FakeFs {
|
|||
&self,
|
||||
path: &Path,
|
||||
_: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>> {
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
self.simulate_random_delay().await;
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
self.state.lock().event_txs.push(tx);
|
||||
let path = path.to_path_buf();
|
||||
let executor = self.executor.clone();
|
||||
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();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
#[cfg(target_os = "macos")]
|
||||
pub use mac_impl::*;
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_impl;
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Event {
|
||||
|
@ -14,10 +20,244 @@ pub struct Event {
|
|||
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
|
||||
// /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Headers/FSEvents.h
|
||||
bitflags! {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct StreamFlags: u32 {
|
||||
const NONE = 0x00000000;
|
||||
|
@ -121,3 +361,138 @@ impl std::fmt::Display for StreamFlags {
|
|||
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> {
|
||||
Err(anyhow::Error::msg(
|
||||
"Platform<LinuxPlatform>::app_path is not implemented yet",
|
||||
|
@ -338,7 +338,7 @@ impl Platform for LinuxPlatform {
|
|||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
//TODO linux
|
||||
//todo(linux)
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
"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>)>>> {
|
||||
let url = url.to_string();
|
||||
self.background_executor().spawn(async move {
|
||||
|
|
|
@ -281,7 +281,7 @@ impl Client for WaylandClient {
|
|||
CursorStyle::ResizeUp => "n-resize".to_string(),
|
||||
CursorStyle::ResizeDown => "s-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::OperationNotAllowed => "not-allowed".to_string(),
|
||||
CursorStyle::DragLink => "dnd-link".to_string(),
|
||||
|
|
|
@ -361,7 +361,7 @@ impl Client for X11Client {
|
|||
Box::new(X11Window(window_ptr))
|
||||
}
|
||||
|
||||
// TODO linux
|
||||
//todo(linux)
|
||||
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||
|
||||
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 _;
|
||||
|
||||
// Populate ignores above the root.
|
||||
|
@ -3389,11 +3389,9 @@ impl BackgroundScanner {
|
|||
// For these events, update events cannot be as precise, because we didn't
|
||||
// have the previous state loaded yet.
|
||||
self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
|
||||
if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) {
|
||||
let mut paths = fs::fs_events_paths(events);
|
||||
|
||||
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
|
||||
paths.extend(fs::fs_events_paths(more_events));
|
||||
if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) {
|
||||
while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
|
||||
paths.extend(more_paths);
|
||||
}
|
||||
self.process_events(paths).await;
|
||||
}
|
||||
|
@ -3430,12 +3428,10 @@ impl BackgroundScanner {
|
|||
}
|
||||
}
|
||||
|
||||
events = fs_events_rx.next().fuse() => {
|
||||
let Some(events) = events else { break };
|
||||
let mut paths = fs::fs_events_paths(events);
|
||||
|
||||
while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) {
|
||||
paths.extend(fs::fs_events_paths(more_events));
|
||||
paths = fs_events_rx.next().fuse() => {
|
||||
let Some(mut paths) = paths else { break };
|
||||
while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
|
||||
paths.extend(more_paths);
|
||||
}
|
||||
self.process_events(paths.clone()).await;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ extensions_ui.workspace = true
|
|||
feedback.workspace = true
|
||||
file_finder.workspace = true
|
||||
fs.workspace = true
|
||||
fsevent.workspace = true
|
||||
futures.workspace = true
|
||||
go_to_line.workspace = true
|
||||
gpui.workspace = true
|
||||
|
|
|
@ -11,8 +11,6 @@ use db::kvp::KEY_VALUE_STORE;
|
|||
use editor::Editor;
|
||||
use env_logger::Builder;
|
||||
use fs::RealFs;
|
||||
#[cfg(target_os = "macos")]
|
||||
use fsevent::StreamFlags;
|
||||
use futures::{future, StreamExt};
|
||||
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
|
||||
use isahc::{prelude::Configurable, Request};
|
||||
|
@ -184,7 +182,6 @@ fn main() {
|
|||
);
|
||||
|
||||
load_user_themes_in_background(fs.clone(), cx);
|
||||
#[cfg(target_os = "macos")]
|
||||
watch_themes(fs.clone(), cx);
|
||||
|
||||
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() {
|
||||
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::DB_DIR).expect("could not create database 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");
|
||||
}
|
||||
|
||||
|
@ -974,9 +972,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
|||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
// todo(linux): Port fsevents to linux
|
||||
/// 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) {
|
||||
use std::time::Duration;
|
||||
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))
|
||||
.await;
|
||||
|
||||
while let Some(events) = events.next().await {
|
||||
for event in events {
|
||||
if event.flags.contains(StreamFlags::ITEM_REMOVED) {
|
||||
// Theme was removed, don't need to reload.
|
||||
// We may want to remove the theme from the registry, in this case.
|
||||
} else {
|
||||
while let Some(paths) = events.next().await {
|
||||
for path in paths {
|
||||
if fs.metadata(&path).await.ok().flatten().is_some() {
|
||||
if let Some(theme_registry) =
|
||||
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
|
||||
{
|
||||
if let Some(()) = theme_registry
|
||||
.load_user_theme(&event.path, fs.clone())
|
||||
.load_user_theme(&path, fs.clone())
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
|
|
|
@ -47,10 +47,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
|
|||
clippy_command.arg("--workspace");
|
||||
}
|
||||
|
||||
clippy_command
|
||||
.arg("--release")
|
||||
.arg("--all-targets")
|
||||
.arg("--all-features");
|
||||
clippy_command.arg("--release").arg("--all-features");
|
||||
|
||||
if args.fix {
|
||||
clippy_command.arg("--fix");
|
||||
|
@ -60,6 +57,7 @@ fn run_clippy(args: ClippyArgs) -> Result<()> {
|
|||
|
||||
// Deny all warnings.
|
||||
// We don't do this yet on Windows, as it still has some warnings present.
|
||||
// todo(windows)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
clippy_command.args(["--deny", "warnings"]);
|
||||
|
||||
|
|
Loading…
Reference in a new issue