Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: filter file watcher duplicate events #200

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -67,6 +67,7 @@ two-face = "0.3.0"
# Required for WGPU to work properly with Vulkan
fontdb = { version = "0.13.1", features = ["fontconfig"] }
human-panic = "1.2.2"
notify-debouncer-full = { version = "0.3.1", default-features = false }

[dependencies.glyphon]
version = "0.2"
Expand Down
59 changes: 48 additions & 11 deletions src/file_watcher/mod.rs
Expand Up @@ -8,7 +8,10 @@ use std::time::Duration;
use crate::InlyneEvent;

use notify::event::{EventKind, ModifyKind};
use notify::{Event, EventHandler, RecommendedWatcher, RecursiveMode, Watcher as _};
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
use notify_debouncer_full::{
new_debouncer, DebounceEventHandler, DebounceEventResult, Debouncer, FileIdMap,
};
use winit::event_loop::EventLoopProxy;

trait Callback: Send + 'static {
Expand All @@ -35,10 +38,43 @@ enum WatcherMsg {

struct MsgHandler(mpsc::Sender<WatcherMsg>);

impl EventHandler for MsgHandler {
fn handle_event(&mut self, event: notify::Result<Event>) {
let msg = WatcherMsg::Notify(event);
let _ = self.0.send(msg);
impl DebounceEventHandler for MsgHandler {
fn handle_event(&mut self, debounced_event: DebounceEventResult) {
log::debug!("Received debounced file events: {:#?}", debounced_event);

match debounced_event {
Ok(events) => {
let mut selected_event: Option<Event> = None;

// select the most interesting event
// Rename/Remove is more interesting than changing the contents
for ev in events {
match ev.event.kind {
EventKind::Modify(ModifyKind::Name(_)) | EventKind::Remove(_) => {
let _ = selected_event.insert(ev.event);
}
EventKind::Create(_) | EventKind::Modify(_) => {
let _ = selected_event.get_or_insert(ev.event);
}
_ => {}
}
}

if let Some(event) = selected_event {
let msg = WatcherMsg::Notify(Ok(event));
let _ = self.0.send(msg);
} else {
log::trace!("Ignoring events")
}
}
Err(err) => {
// log all errors
for e in err {
let msg = WatcherMsg::Notify(Err(e));
let _ = self.0.send(msg);
}
}
}
}
}

Expand All @@ -54,7 +90,7 @@ impl Watcher {
let watcher = Self(msg_tx.clone());

let notify_watcher =
RecommendedWatcher::new(MsgHandler(msg_tx), notify::Config::default()).unwrap();
new_debouncer(Duration::from_millis(10), None, MsgHandler(msg_tx)).unwrap();

std::thread::spawn(move || {
endlessly_handle_messages(notify_watcher, msg_rx, reload_callback, file_path);
Expand All @@ -73,11 +109,12 @@ impl Watcher {
}

fn endlessly_handle_messages<C: Callback>(
mut watcher: RecommendedWatcher,
mut watcher: Debouncer<RecommendedWatcher, FileIdMap>,
msg_rx: mpsc::Receiver<WatcherMsg>,
reload_callback: C,
mut file_path: PathBuf,
) {
let watcher = watcher.watcher();
watcher
.watch(&file_path, RecursiveMode::NonRecursive)
.unwrap();
Expand All @@ -97,17 +134,17 @@ fn endlessly_handle_messages<C: Callback>(
while let Ok(msg) = msg_rx.recv() {
match msg {
WatcherMsg::Notify(Ok(event)) => {
log::trace!("File event: {:#?}", event);
log::debug!("Processing file event: {:#?}", event);

if matches!(
event.kind,
EventKind::Remove(_) | EventKind::Modify(ModifyKind::Name(_))
) {
log::debug!("File may have been renamed/removed. Falling back to polling");
poll_registering_watcher(&mut watcher, &file_path);
poll_registering_watcher(watcher, &file_path);
log::debug!("Successfully re-registered file watcher");
reload_callback.file_reload();
} else if matches!(event.kind, EventKind::Modify(_)) {
} else if matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_)) {
log::debug!("Reloading file");
reload_callback.file_reload();
}
Expand All @@ -116,7 +153,7 @@ fn endlessly_handle_messages<C: Callback>(
WatcherMsg::FileChange { new_path, contents } => {
log::info!("Updating file watcher path: {}", new_path.display());
let _ = watcher.unwatch(&file_path);
poll_registering_watcher(&mut watcher, &new_path);
poll_registering_watcher(watcher, &new_path);
file_path = new_path;
reload_callback.file_change(contents);
}
Expand Down
11 changes: 9 additions & 2 deletions src/file_watcher/tests.rs
Expand Up @@ -61,6 +61,8 @@ impl Delays {
}

struct TestEnv {
// TODO: no need to pass this into the test env. Keep it external and limit what gets passed to
// tests
temp_dir: TempDir,
main_file: PathBuf,
rel_file: PathBuf,
Expand Down Expand Up @@ -108,8 +110,13 @@ macro_rules! gen_watcher_test {
for _ in 0..4 {
let result = std::panic::catch_unwind(|| {
let test_dir = TestEnv::init();
// Give the watcher time to get comfy :)
delays.delay();
// Give the watcher time to get comfy :)
delays.delay();

// For some reason it looks like MacOS gets a create event even though the
// watcher gets registered after the file is already created. Drain any
// initial notifications to start
while test_dir.callback_rx.recv_timeout(delays.short_timeout).is_ok() {}

$test_fn(test_dir, delays.clone())
});
Expand Down
5 changes: 5 additions & 0 deletions src/interpreter/mod.rs
Expand Up @@ -166,6 +166,11 @@ impl HtmlInterpreter {
let mut tok = Tokenizer::new(self, TokenizerOpts::default());

for md_string in receiver {
log::debug!(
"Recieved markdown for interpretation: {} bytes",
md_string.len()
);

if tok
.sink
.should_queue
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Expand Up @@ -756,6 +756,7 @@ fn main() -> anyhow::Result<()> {
human_panic::setup_panic!();

env_logger::Builder::new()
.format_timestamp_millis()
.filter_level(log::LevelFilter::Error)
.filter_module("inlyne", log::LevelFilter::Info)
.parse_env("INLYNE_LOG")
Expand Down
2 changes: 1 addition & 1 deletion src/test_utils.rs
Expand Up @@ -3,7 +3,7 @@ use log::LevelFilter;
pub fn init_test_log() {
// Ignore errors because other tests in the same binary may have already initialized the logger
let _ = env_logger::Builder::new()
.filter(Some("inlyne"), LevelFilter::Debug)
.filter(Some("inlyne"), LevelFilter::Trace)
.filter(None, LevelFilter::Warn)
.is_test(true)
.try_init();
Expand Down