From d4fa61e4358bdc83173c346121c992836f1b7152 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 26 Sep 2024 12:26:24 +0200 Subject: [PATCH] Debounce file change events --- src/watch.rs | 8 ++-- src/watch/notify_event.rs | 91 +++++++++++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index fd89b29e..11450b49 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -69,11 +69,11 @@ fn run_watch( // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. let _watcher_guard = if let Some(exercise_names) = notify_exercise_names { + let notify_event_handler = + NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?; + let mut watcher = RecommendedWatcher::new( - NotifyEventHandler { - sender: watch_event_sender.clone(), - exercise_names, - }, + notify_event_handler, Config::default().with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 5ed8fd18..2051e544 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,15 +1,71 @@ +use anyhow::{Context, Result}; use notify::{ - event::{MetadataKind, ModifyKind}, + event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode}, Event, EventKind, }; -use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; +use std::{ + sync::{ + atomic::Ordering::Relaxed, + mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender}, + }, + thread, + time::Duration, +}; use super::{WatchEvent, EXERCISE_RUNNING}; +const DEBOUNCE_DURATION: Duration = Duration::from_millis(200); + pub struct NotifyEventHandler { - pub sender: Sender, - /// Used to report which exercise was modified. - pub exercise_names: &'static [&'static [u8]], + error_sender: Sender, + // Sends the index of the updated exercise. + update_sender: SyncSender, + // Used to report which exercise was modified. + exercise_names: &'static [&'static [u8]], +} + +impl NotifyEventHandler { + pub fn build( + watch_event_sender: Sender, + exercise_names: &'static [&'static [u8]], + ) -> Result { + let (update_sender, update_receiver) = sync_channel(0); + let error_sender = watch_event_sender.clone(); + + // Debouncer + thread::Builder::new() + .spawn(move || { + let mut exercise_updated = vec![false; exercise_names.len()]; + + loop { + match update_receiver.recv_timeout(DEBOUNCE_DURATION) { + Ok(exercise_ind) => exercise_updated[exercise_ind] = true, + Err(RecvTimeoutError::Timeout) => { + for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() { + if *updated { + if watch_event_sender + .send(WatchEvent::FileChange { exercise_ind }) + .is_err() + { + break; + } + + *updated = false; + } + } + } + Err(RecvTimeoutError::Disconnected) => break, + } + } + }) + .context("Failed to spawn a thread to debounce file changes")?; + + Ok(Self { + error_sender, + update_sender, + exercise_names, + }) + } } impl notify::EventHandler for NotifyEventHandler { @@ -22,8 +78,8 @@ impl notify::EventHandler for NotifyEventHandler { Ok(v) => v, Err(e) => { // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.sender.send(WatchEvent::NotifyErr(e)); + // After dropping the receiver, the watcher guard should also be dropped. + let _ = self.error_sender.send(WatchEvent::NotifyErr(e)); return; } }; @@ -32,6 +88,10 @@ impl notify::EventHandler for NotifyEventHandler { EventKind::Any => (), EventKind::Modify(modify_kind) => match modify_kind { ModifyKind::Any | ModifyKind::Data(_) => (), + ModifyKind::Name(rename_mode) => match rename_mode { + RenameMode::Any | RenameMode::To => (), + RenameMode::From | RenameMode::Both | RenameMode::Other => return, + }, ModifyKind::Metadata(metadata_kind) => match metadata_kind { MetadataKind::Any | MetadataKind::WriteTime => (), MetadataKind::AccessTime @@ -40,12 +100,17 @@ impl notify::EventHandler for NotifyEventHandler { | MetadataKind::Extended | MetadataKind::Other => return, }, - ModifyKind::Name(_) | ModifyKind::Other => return, + ModifyKind::Other => return, }, - EventKind::Access(_) - | EventKind::Create(_) - | EventKind::Remove(_) - | EventKind::Other => return, + EventKind::Access(access_kind) => match access_kind { + AccessKind::Any => (), + AccessKind::Close(access_mode) => match access_mode { + AccessMode::Any | AccessMode::Write => (), + AccessMode::Execute | AccessMode::Read | AccessMode::Other => return, + }, + AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return, + }, + EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return, } let _ = input_event @@ -62,6 +127,6 @@ impl notify::EventHandler for NotifyEventHandler { .iter() .position(|exercise_name| *exercise_name == file_name_without_ext) }) - .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); + .try_for_each(|exercise_ind| self.update_sender.send(exercise_ind)); } }