From ed242d19efbb5b8c5a0fd72d4f14419a830a4f85 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 17 Jun 2022 00:10:15 +0200 Subject: [PATCH 1/8] Add debouncer --- src/debouncer.rs | 230 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 2 files changed, 233 insertions(+) create mode 100644 src/debouncer.rs diff --git a/src/debouncer.rs b/src/debouncer.rs new file mode 100644 index 00000000..7e27ae1b --- /dev/null +++ b/src/debouncer.rs @@ -0,0 +1,230 @@ +//! Debouncer & access code +use std::{ + collections::HashMap, + path::PathBuf, + sync::{ + mpsc::{self, Receiver}, + Arc, Mutex, MutexGuard, + }, + time::{Duration, Instant}, +}; + +use crate::{ + event::{MetadataKind, ModifyKind}, + Error, ErrorKind, Event, EventKind, RecommendedWatcher, Watcher, +}; + +/// Deduplicate event data entry +struct EventData { + /// Deduplicated event + kind: DebouncedEvent, + /// Insertion Time + insert: Instant, + /// Last Update + update: Instant, +} + +/// A debounced event. Do note that any precise events are heavily platform dependent and only Any is gauranteed to work in all cases. +/// See also https://github.com/notify-rs/notify/wiki/The-Event-Guide#platform-specific-behaviour for more information. +#[derive(Eq, PartialEq, Clone)] +pub enum DebouncedEvent { + // NoticeWrite(PathBuf), + // NoticeRemove(PathBuf), + /// When precise events are disabled for files + Any, + /// Access performed + Access, + /// File created + Create, + /// Write performed + Write, + /// Write performed but debounce timed out (continuous writes) + ContinuousWrite, + /// Metadata change like permissions + Metadata, + /// File deleted + Remove, + // Rename(PathBuf, PathBuf), + // Rescan, + // Error(Error, Option), +} + +impl From for EventData { + fn from(e: DebouncedEvent) -> Self { + let start = Instant::now(); + EventData { + kind: e, + insert: start.clone(), + update: start, + } + } +} + +type DebounceData = Arc>; + +#[derive(Default)] +struct DebounceDataInner { + d: HashMap, + timeout: Duration, +} + +impl DebounceDataInner { + /// Retrieve a vec of debounced events, removing them if not continuous + pub fn debounced_events(&mut self) -> HashMap { + let mut events_expired = HashMap::new(); + let mut data_back = HashMap::new(); + // TODO: perfect fit for drain_filter https://github.com/rust-lang/rust/issues/59618 + for (k, v) in self.d.drain() { + if v.update.elapsed() >= self.timeout { + events_expired.insert(k, v.kind); + } else if v.kind == DebouncedEvent::Write && v.insert.elapsed() >= self.timeout { + // TODO: allow config for continuous writes reports + data_back.insert(k.clone(), v); + events_expired.insert(k, DebouncedEvent::ContinuousWrite); + } else { + data_back.insert(k, v); + } + } + self.d = data_back; + events_expired + } + + /// Helper to insert or update EventData + fn _insert_event(&mut self, path: PathBuf, kind: DebouncedEvent) { + if let Some(v) = self.d.get_mut(&path) { + // TODO: is it more efficient to take a &EventKind, compare v.kind == kind and only + // update the v.update Instant, trading a .clone() with a compare ? + v.update = Instant::now(); + v.kind = kind; + } else { + self.d.insert(path, kind.into()); + } + } + + /// Add new event to debouncer cache + pub fn add_event(&mut self, e: Event) { + // TODO: handle renaming of backup files as in https://docs.rs/notify/4.0.15/notify/trait.Watcher.html#advantages + match &e.kind { + EventKind::Any | EventKind::Other => { + for p in e.paths.into_iter() { + if let Some(existing) = self.d.get(&p) { + match existing.kind { + DebouncedEvent::Any => (), + _ => continue, + } + } + self._insert_event(p, DebouncedEvent::Any); + } + } + EventKind::Access(_t) => { + for p in e.paths.into_iter() { + if let Some(existing) = self.d.get(&p) { + match existing.kind { + DebouncedEvent::Any | DebouncedEvent::Access => (), + _ => continue, + } + } + self._insert_event(p, DebouncedEvent::Access); + } + } + EventKind::Modify(mod_kind) => { + let target_event = match mod_kind { + // ignore + ModifyKind::Any | ModifyKind::Other => return, + ModifyKind::Data(_) => DebouncedEvent::Write, + ModifyKind::Metadata(_) => DebouncedEvent::Metadata, + // TODO: handle renames + ModifyKind::Name(_) => return, + }; + for p in e.paths.into_iter() { + if let Some(existing) = self.d.get(&p) { + // TODO: consider EventKind::Any on invalid configurations + match existing.kind { + DebouncedEvent::Access + | DebouncedEvent::Any + | DebouncedEvent::Metadata => (), + DebouncedEvent::Write => { + // don't overwrite Write with Metadata event + if target_event != DebouncedEvent::Write { + continue; + } + } + _ => continue, + } + } + self._insert_event(p, target_event.clone()); + } + } + EventKind::Remove(_) => { + // ignore previous events, override + for p in e.paths.into_iter() { + self._insert_event(p, DebouncedEvent::Remove); + } + } + EventKind::Create(_) => { + // override anything except for previous Remove events + for p in e.paths.into_iter() { + if let Some(e) = self.d.get(&p) { + if e.kind == DebouncedEvent::Remove { + // change to write + self._insert_event(p, DebouncedEvent::Write); + continue; + } + } + self._insert_event(p, DebouncedEvent::Create); + } + } + } + } +} + +/// Creates a new debounced watcher +pub fn new_debouncer( + timeout: Duration, +) -> Result< + ( + Receiver>, + RecommendedWatcher, + ), + Error, +> { + let data = DebounceData::default(); + + let (tx, rx) = mpsc::channel(); + + let data_c = data.clone(); + // TODO: do we want to add some ticking option ? + let tick_div = 4; + // TODO: use proper error kind (like InvalidConfig that requires passing a Config) + let tick = timeout.checked_div(tick_div).ok_or_else(|| { + Error::new(ErrorKind::Generic(format!( + "Failed to calculate tick as {:?}/{}!", + timeout, tick_div + ))) + })?; + std::thread::spawn(move || { + loop { + std::thread::sleep(tick); + let send_data; + { + let mut lock = data_c.lock().expect("Can't lock debouncer data!"); + send_data = lock.debounced_events(); + } + if send_data.len() > 0 { + // TODO: how do we efficiently detect an rx drop without sending data ? + if tx.send(send_data).is_err() { + break; + } + } + } + }); + + let watcher = RecommendedWatcher::new_immediate(move |e: Result| { + if let Ok(e) = e { + let mut lock = data.lock().expect("Can't lock debouncer data!"); + lock.add_event(e); + } + })?; + + Ok((rx, watcher)) +} diff --git a/src/lib.rs b/src/lib.rs index 603adffa..41e623f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,8 +101,10 @@ #![deny(missing_docs)] pub use config::{Config, RecursiveMode}; +pub use debouncer::new_debouncer; pub use error::{Error, ErrorKind, Result}; pub use event::{Event, EventKind}; +use std::convert::AsRef; use std::path::Path; #[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))] @@ -142,6 +144,7 @@ pub mod null; pub mod poll; mod config; +mod debouncer; mod error; /// The set of requirements for watcher event handling functions. From e9fbc59b6c0fd93647f5dd965ec526acc13ce759 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 17 Jun 2022 00:26:29 +0200 Subject: [PATCH 2/8] cleanup --- src/debouncer.rs | 11 +++-------- src/lib.rs | 1 - 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/debouncer.rs b/src/debouncer.rs index 7e27ae1b..2ced88a5 100644 --- a/src/debouncer.rs +++ b/src/debouncer.rs @@ -4,13 +4,13 @@ use std::{ path::PathBuf, sync::{ mpsc::{self, Receiver}, - Arc, Mutex, MutexGuard, + Arc, Mutex, }, time::{Duration, Instant}, }; use crate::{ - event::{MetadataKind, ModifyKind}, + event::ModifyKind, Error, ErrorKind, Event, EventKind, RecommendedWatcher, Watcher, }; @@ -28,8 +28,6 @@ struct EventData { /// See also https://github.com/notify-rs/notify/wiki/The-Event-Guide#platform-specific-behaviour for more information. #[derive(Eq, PartialEq, Clone)] pub enum DebouncedEvent { - // NoticeWrite(PathBuf), - // NoticeRemove(PathBuf), /// When precise events are disabled for files Any, /// Access performed @@ -44,9 +42,6 @@ pub enum DebouncedEvent { Metadata, /// File deleted Remove, - // Rename(PathBuf, PathBuf), - // Rescan, - // Error(Error, Option), } impl From for EventData { @@ -219,7 +214,7 @@ pub fn new_debouncer( } }); - let watcher = RecommendedWatcher::new_immediate(move |e: Result| { + let watcher = RecommendedWatcher::new(move |e: Result| { if let Ok(e) = e { let mut lock = data.lock().expect("Can't lock debouncer data!"); lock.add_event(e); diff --git a/src/lib.rs b/src/lib.rs index 41e623f8..0c45d51a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,7 +104,6 @@ pub use config::{Config, RecursiveMode}; pub use debouncer::new_debouncer; pub use error::{Error, ErrorKind, Result}; pub use event::{Event, EventKind}; -use std::convert::AsRef; use std::path::Path; #[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))] From 86a12037ffefd10b68c64fc170a375fe4fc9be54 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 17 Jun 2022 15:29:54 +0200 Subject: [PATCH 3/8] remove smart event kind handler in debouncer --- src/debouncer.rs | 173 +++++++++++++---------------------------------- 1 file changed, 47 insertions(+), 126 deletions(-) diff --git a/src/debouncer.rs b/src/debouncer.rs index 2ced88a5..90f55ec3 100644 --- a/src/debouncer.rs +++ b/src/debouncer.rs @@ -1,4 +1,6 @@ //! Debouncer & access code +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, path::PathBuf, @@ -9,49 +11,52 @@ use std::{ time::{Duration, Instant}, }; -use crate::{ - event::ModifyKind, - Error, ErrorKind, Event, EventKind, RecommendedWatcher, Watcher, -}; +use crate::{Error, ErrorKind, Event, RecommendedWatcher, Watcher}; /// Deduplicate event data entry struct EventData { - /// Deduplicated event - kind: DebouncedEvent, /// Insertion Time insert: Instant, /// Last Update update: Instant, } -/// A debounced event. Do note that any precise events are heavily platform dependent and only Any is gauranteed to work in all cases. -/// See also https://github.com/notify-rs/notify/wiki/The-Event-Guide#platform-specific-behaviour for more information. -#[derive(Eq, PartialEq, Clone)] -pub enum DebouncedEvent { +impl EventData { + fn new_any() -> Self { + let time = Instant::now(); + Self { + insert: time.clone(), + update: time, + } + } +} + +/// A debounced event kind. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] +pub enum DebouncedEventKind { /// When precise events are disabled for files Any, - /// Access performed - Access, - /// File created - Create, - /// Write performed - Write, - /// Write performed but debounce timed out (continuous writes) - ContinuousWrite, - /// Metadata change like permissions - Metadata, - /// File deleted - Remove, + /// Event but debounce timed out (for example continuous writes) + AnyContinuous, } -impl From for EventData { - fn from(e: DebouncedEvent) -> Self { - let start = Instant::now(); - EventData { - kind: e, - insert: start.clone(), - update: start, - } +/// A debounced event. +/// +/// Does not emit any specific event type on purpose, only distinguishes between an any event and a continuous any event. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct DebouncedEvent { + /// Event path + pub path: PathBuf, + /// Event kind + pub kind: DebouncedEventKind, +} + +impl DebouncedEvent { + fn new(path: PathBuf, kind: DebouncedEventKind) -> Self { + Self { path, kind } } } @@ -65,17 +70,16 @@ struct DebounceDataInner { impl DebounceDataInner { /// Retrieve a vec of debounced events, removing them if not continuous - pub fn debounced_events(&mut self) -> HashMap { - let mut events_expired = HashMap::new(); - let mut data_back = HashMap::new(); + pub fn debounced_events(&mut self) -> Vec { + let mut events_expired = Vec::with_capacity(self.d.len()); + let mut data_back = HashMap::with_capacity(self.d.len()); // TODO: perfect fit for drain_filter https://github.com/rust-lang/rust/issues/59618 for (k, v) in self.d.drain() { if v.update.elapsed() >= self.timeout { - events_expired.insert(k, v.kind); - } else if v.kind == DebouncedEvent::Write && v.insert.elapsed() >= self.timeout { - // TODO: allow config for continuous writes reports + events_expired.push(DebouncedEvent::new(k, DebouncedEventKind::Any)); + } else if v.insert.elapsed() >= self.timeout { data_back.insert(k.clone(), v); - events_expired.insert(k, DebouncedEvent::ContinuousWrite); + events_expired.push(DebouncedEvent::new(k, DebouncedEventKind::AnyContinuous)); } else { data_back.insert(k, v); } @@ -84,90 +88,13 @@ impl DebounceDataInner { events_expired } - /// Helper to insert or update EventData - fn _insert_event(&mut self, path: PathBuf, kind: DebouncedEvent) { - if let Some(v) = self.d.get_mut(&path) { - // TODO: is it more efficient to take a &EventKind, compare v.kind == kind and only - // update the v.update Instant, trading a .clone() with a compare ? - v.update = Instant::now(); - v.kind = kind; - } else { - self.d.insert(path, kind.into()); - } - } - /// Add new event to debouncer cache pub fn add_event(&mut self, e: Event) { - // TODO: handle renaming of backup files as in https://docs.rs/notify/4.0.15/notify/trait.Watcher.html#advantages - match &e.kind { - EventKind::Any | EventKind::Other => { - for p in e.paths.into_iter() { - if let Some(existing) = self.d.get(&p) { - match existing.kind { - DebouncedEvent::Any => (), - _ => continue, - } - } - self._insert_event(p, DebouncedEvent::Any); - } - } - EventKind::Access(_t) => { - for p in e.paths.into_iter() { - if let Some(existing) = self.d.get(&p) { - match existing.kind { - DebouncedEvent::Any | DebouncedEvent::Access => (), - _ => continue, - } - } - self._insert_event(p, DebouncedEvent::Access); - } - } - EventKind::Modify(mod_kind) => { - let target_event = match mod_kind { - // ignore - ModifyKind::Any | ModifyKind::Other => return, - ModifyKind::Data(_) => DebouncedEvent::Write, - ModifyKind::Metadata(_) => DebouncedEvent::Metadata, - // TODO: handle renames - ModifyKind::Name(_) => return, - }; - for p in e.paths.into_iter() { - if let Some(existing) = self.d.get(&p) { - // TODO: consider EventKind::Any on invalid configurations - match existing.kind { - DebouncedEvent::Access - | DebouncedEvent::Any - | DebouncedEvent::Metadata => (), - DebouncedEvent::Write => { - // don't overwrite Write with Metadata event - if target_event != DebouncedEvent::Write { - continue; - } - } - _ => continue, - } - } - self._insert_event(p, target_event.clone()); - } - } - EventKind::Remove(_) => { - // ignore previous events, override - for p in e.paths.into_iter() { - self._insert_event(p, DebouncedEvent::Remove); - } - } - EventKind::Create(_) => { - // override anything except for previous Remove events - for p in e.paths.into_iter() { - if let Some(e) = self.d.get(&p) { - if e.kind == DebouncedEvent::Remove { - // change to write - self._insert_event(p, DebouncedEvent::Write); - continue; - } - } - self._insert_event(p, DebouncedEvent::Create); - } + for path in e.paths.into_iter() { + if let Some(v) = self.d.get_mut(&path) { + v.update = Instant::now(); + } else { + self.d.insert(path, EventData::new_any()); } } } @@ -176,13 +103,7 @@ impl DebounceDataInner { /// Creates a new debounced watcher pub fn new_debouncer( timeout: Duration, -) -> Result< - ( - Receiver>, - RecommendedWatcher, - ), - Error, -> { +) -> Result<(Receiver>, RecommendedWatcher), Error> { let data = DebounceData::default(); let (tx, rx) = mpsc::channel(); From baa22c85a67bc1c7e78e1fe92affc22c311cf796 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 17 Jun 2022 22:00:24 +0200 Subject: [PATCH 4/8] send errors in debouncer, add example, name thread --- examples/debounced.rs | 26 +++++++++++++++++ src/debouncer.rs | 65 +++++++++++++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 examples/debounced.rs diff --git a/examples/debounced.rs b/examples/debounced.rs new file mode 100644 index 00000000..249f354c --- /dev/null +++ b/examples/debounced.rs @@ -0,0 +1,26 @@ +use std::{path::Path, time::Duration}; + +use notify::{new_debouncer, RecursiveMode, Watcher}; + +fn main() { + std::thread::spawn(|| { + let path = Path::new("test.txt"); + let _ = std::fs::remove_file(&path); + loop { + std::fs::write(&path, b"Lorem ipsum").unwrap(); + std::thread::sleep(Duration::from_millis(250)); + } + }); + + let (rx, mut watcher) = new_debouncer(Duration::from_secs(2)).unwrap(); + + watcher + .watch(Path::new("."), RecursiveMode::Recursive) + .unwrap(); + + for events in rx { + for e in events { + println!("{:?}", e); + } + } +} diff --git a/src/debouncer.rs b/src/debouncer.rs index 90f55ec3..5daf725e 100644 --- a/src/debouncer.rs +++ b/src/debouncer.rs @@ -31,6 +31,8 @@ impl EventData { } } +type DebounceChannelType = Result,Vec>; + /// A debounced event kind. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -66,6 +68,7 @@ type DebounceData = Arc>; struct DebounceDataInner { d: HashMap, timeout: Duration, + e: Vec, } impl DebounceDataInner { @@ -88,11 +91,24 @@ impl DebounceDataInner { events_expired } + /// Returns all currently stored errors + pub fn errors(&mut self) -> Vec { + let mut v = Vec::new(); + std::mem::swap(&mut v, &mut self.e); + v + } + + /// Add an error entry to re-send later on + pub fn add_error(&mut self, e: crate::Error) { + self.e.push(e); + } + /// Add new event to debouncer cache pub fn add_event(&mut self, e: Event) { for path in e.paths.into_iter() { if let Some(v) = self.d.get_mut(&path) { v.update = Instant::now(); + println!("Exists"); } else { self.d.insert(path, EventData::new_any()); } @@ -103,7 +119,7 @@ impl DebounceDataInner { /// Creates a new debounced watcher pub fn new_debouncer( timeout: Duration, -) -> Result<(Receiver>, RecommendedWatcher), Error> { +) -> Result<(Receiver, RecommendedWatcher), Error> { let data = DebounceData::default(); let (tx, rx) = mpsc::channel(); @@ -118,27 +134,40 @@ pub fn new_debouncer( timeout, tick_div ))) })?; - std::thread::spawn(move || { - loop { - std::thread::sleep(tick); - let send_data; - { - let mut lock = data_c.lock().expect("Can't lock debouncer data!"); - send_data = lock.debounced_events(); - } - if send_data.len() > 0 { - // TODO: how do we efficiently detect an rx drop without sending data ? - if tx.send(send_data).is_err() { - break; + std::thread::Builder::new() + .name("notify-rs debouncer loop".to_string()) + .spawn(move || { + loop { + std::thread::sleep(tick); + let send_data; + let errors: Vec; + { + let mut lock = data_c.lock().expect("Can't lock debouncer data!"); + send_data = lock.debounced_events(); + errors = lock.errors(); + } + if send_data.len() > 0 { + // channel shut down + if tx.send(Ok(send_data)).is_err() { + break; + } + } + if errors.len() > 0 { + // channel shut down + if tx.send(Err(errors)).is_err() { + break; + } } } - } - }); + })?; let watcher = RecommendedWatcher::new(move |e: Result| { - if let Ok(e) = e { - let mut lock = data.lock().expect("Can't lock debouncer data!"); - lock.add_event(e); + let mut lock = data.lock().expect("Can't lock debouncer data!"); + + match e { + Ok(e) => lock.add_event(e), + // can't have multiple TX, so we need to pipe that through our debouncer + Err(e) => lock.add_error(e), } })?; From 2e3a2acc8f0c01a5630203e29415d5e7ec969f8e Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 17 Jun 2022 22:14:14 +0200 Subject: [PATCH 5/8] Allow selecting a tick_rate for the debouncer --- examples/debounced.rs | 2 +- src/debouncer.rs | 45 ++++++++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/examples/debounced.rs b/examples/debounced.rs index 249f354c..259a01c6 100644 --- a/examples/debounced.rs +++ b/examples/debounced.rs @@ -12,7 +12,7 @@ fn main() { } }); - let (rx, mut watcher) = new_debouncer(Duration::from_secs(2)).unwrap(); + let (rx, mut watcher) = new_debouncer(Duration::from_secs(2), None).unwrap(); watcher .watch(Path::new("."), RecursiveMode::Recursive) diff --git a/src/debouncer.rs b/src/debouncer.rs index 5daf725e..9ad648d3 100644 --- a/src/debouncer.rs +++ b/src/debouncer.rs @@ -31,7 +31,7 @@ impl EventData { } } -type DebounceChannelType = Result,Vec>; +type DebounceChannelType = Result, Vec>; /// A debounced event kind. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -79,8 +79,10 @@ impl DebounceDataInner { // TODO: perfect fit for drain_filter https://github.com/rust-lang/rust/issues/59618 for (k, v) in self.d.drain() { if v.update.elapsed() >= self.timeout { + println!("normal timeout"); events_expired.push(DebouncedEvent::new(k, DebouncedEventKind::Any)); } else if v.insert.elapsed() >= self.timeout { + println!("continuous"); data_back.insert(k.clone(), v); events_expired.push(DebouncedEvent::new(k, DebouncedEventKind::AnyContinuous)); } else { @@ -116,24 +118,45 @@ impl DebounceDataInner { } } -/// Creates a new debounced watcher +/// Creates a new debounced watcher. +/// +/// Timeout is the amount of time after which a debounced event is emitted or a Continuous event is send, if there still are events incoming for the specific path. +/// +/// If tick_rate is None, notify will select a tick rate that is less than the provided timeout. pub fn new_debouncer( timeout: Duration, + tick_rate: Option, ) -> Result<(Receiver, RecommendedWatcher), Error> { let data = DebounceData::default(); + let tick_div = 4; + let tick = match tick_rate { + Some(v) => { + if v > timeout { + return Err(Error::new(ErrorKind::Generic(format!( + "Invalid tick_rate, tick rate {:?} > {:?} timeout!", + v, timeout + )))); + } + v + } + None => timeout.checked_div(tick_div).ok_or_else(|| { + Error::new(ErrorKind::Generic(format!( + "Failed to calculate tick as {:?}/{}!", + timeout, tick_div + ))) + })?, + }; + + { + let mut data_w = data.lock().unwrap(); + data_w.timeout = timeout; + } + let (tx, rx) = mpsc::channel(); let data_c = data.clone(); - // TODO: do we want to add some ticking option ? - let tick_div = 4; - // TODO: use proper error kind (like InvalidConfig that requires passing a Config) - let tick = timeout.checked_div(tick_div).ok_or_else(|| { - Error::new(ErrorKind::Generic(format!( - "Failed to calculate tick as {:?}/{}!", - timeout, tick_div - ))) - })?; + std::thread::Builder::new() .name("notify-rs debouncer loop".to_string()) .spawn(move || { From 74333bb0230c866c3056153df85e67ebb9fa9cf2 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Tue, 2 Aug 2022 14:10:54 +0200 Subject: [PATCH 6/8] move debouncer to own crate and fix audit failures --- .github/workflows/main.yml | 7 +- Cargo.toml | 75 +++---------------- examples/Cargo.toml | 35 +++++++++ examples/debounced.rs | 3 +- examples/hot_reload_tide/Cargo.toml | 4 +- .../hot_reload_tide/tests/messages_test.rs | 2 +- .../src/main.rs => watcher_kind.rs} | 1 + examples/watcher_kind/Cargo.toml | 10 --- notify-debouncer-mini/Cargo.toml | 14 ++++ .../src/lib.rs | 2 +- notify/.gitignore | 6 ++ notify/Cargo.toml | 54 +++++++++++++ {src => notify/src}/config.rs | 0 {src => notify/src}/error.rs | 0 {src => notify/src}/event.rs | 0 {src => notify/src}/fsevent.rs | 0 {src => notify/src}/inotify.rs | 0 {src => notify/src}/kqueue.rs | 0 {src => notify/src}/lib.rs | 2 - {src => notify/src}/null.rs | 0 {src => notify/src}/poll.rs | 1 - {src => notify/src}/windows.rs | 0 release_checklist.md | 12 ++- 23 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 examples/Cargo.toml rename examples/{watcher_kind/src/main.rs => watcher_kind.rs} (95%) delete mode 100644 examples/watcher_kind/Cargo.toml create mode 100644 notify-debouncer-mini/Cargo.toml rename src/debouncer.rs => notify-debouncer-mini/src/lib.rs (98%) create mode 100644 notify/.gitignore create mode 100644 notify/Cargo.toml rename {src => notify/src}/config.rs (100%) rename {src => notify/src}/error.rs (100%) rename {src => notify/src}/event.rs (100%) rename {src => notify/src}/fsevent.rs (100%) rename {src => notify/src}/inotify.rs (100%) rename {src => notify/src}/kqueue.rs (100%) rename {src => notify/src}/lib.rs (99%) rename {src => notify/src}/null.rs (100%) rename {src => notify/src}/poll.rs (99%) rename {src => notify/src}/windows.rs (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 59cf4748..319beea4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,13 +53,14 @@ jobs: if: matrix.version == '1.56.0' && matrix.os != 'macos-latest' run: cargo check --features=serde - - name: check build example + - name: check build examples if: matrix.version == 'stable' - run: cargo check -p watcher_kind + run: cargo check --package examples --examples - name: test hot_reload_tide if: matrix.version == 'stable' - run: cargo test -p hot_reload_tide + run: cargo test + working-directory: examples/hot_reload_tide - name: test if: matrix.version == 'stable' diff --git a/Cargo.toml b/Cargo.toml index ab0f0de3..af4486c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,69 +1,16 @@ -[package] -name = "notify" -version = "5.0.0-pre.15" -rust-version = "1.56" -description = "Cross-platform filesystem notification library" -documentation = "https://docs.rs/notify" -homepage = "https://github.com/notify-rs/notify" -repository = "https://github.com/notify-rs/notify.git" -readme = "README.md" -license = "CC0-1.0 OR Artistic-2.0" -keywords = ["events", "filesystem", "notify", "watch"] -categories = ["filesystem"] -authors = [ - "Félix Saparelli ", - "Daniel Faust " -] +[workspace] -edition = "2021" -exclude = [ - "/clippy.toml", - ".github/*" +members = [ + "notify", + "notify-debouncer-mini", + + # internal + "examples" + #"examples/hot_reload_tide" untill https://github.com/rustsec/rustsec/issues/501 is resolved ] -[dependencies] -bitflags = "1.0.4" -crossbeam-channel = "0.5.0" -filetime = "0.2.6" -libc = "0.2.4" -serde = { version = "1.0.89", features = ["derive"], optional = true } -walkdir = "2.0.1" - -[target.'cfg(target_os="linux")'.dependencies] -inotify = { version = "0.9", default-features = false } -mio = { version = "0.8", features = ["os-ext"] } - -[target.'cfg(target_os="macos")'.dependencies] -fsevent-sys = { version = "4", optional = true } -kqueue = { version = "1.0", optional = true } -mio = { version = "0.8", features = ["os-ext"], optional = true } - -[target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.8", features = ["fileapi", "handleapi", "ioapiset", "minwinbase", "synchapi", "winbase", "winnt"] } - -[target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd"))'.dependencies] -kqueue = "^1.0.4" # fix for #344 -mio = { version = "0.8", features = ["os-ext"] } - -[dev-dependencies] -futures = "0.3" -serde_json = "1.0.39" -tempfile = "3.2.0" -nix = "0.23.1" - -[features] -default = ["macos_fsevent"] -timing_tests = [] -manual_tests = [] -macos_kqueue = ["kqueue", "mio"] -macos_fsevent = ["fsevent-sys"] +exclude = ["examples/hot_reload_tide"] [patch.crates-io] -notify = { path = "." } - -[workspace] -members = [ - ".", - "examples/hot_reload_tide", - "examples/watcher_kind" -] +notify = { path = "notify/" } +notify-debouncer-mini = { path = "notify-debouncer-mini/" } \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 00000000..6d215db6 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "examples" +version = "0.0.0" +publish = false +edition = "2021" + +[dev-dependencies] +notify = { version = "5.0.0-pre.15" } +notify-debouncer-mini = { version = "0.1" } +futures = "0.3" + +[[example]] +name = "async_monitor" +path = "async_monitor.rs" + +[[example]] +name = "monitor_raw" +path = "monitor_raw.rs" + +[[example]] +name = "debounced" +path = "debounced.rs" + +[[example]] +name = "poll_sysfs" +path = "poll_sysfs.rs" + +[[example]] +name = "watcher_kind" +path = "watcher_kind.rs" + +# specifically in its own sub folder +# to prevent audit from complaining +#[[example]] +#name = "hot_reload_tide" \ No newline at end of file diff --git a/examples/debounced.rs b/examples/debounced.rs index 259a01c6..c3192558 100644 --- a/examples/debounced.rs +++ b/examples/debounced.rs @@ -1,6 +1,7 @@ use std::{path::Path, time::Duration}; -use notify::{new_debouncer, RecursiveMode, Watcher}; +use notify::{RecursiveMode, Watcher}; +use notify_debouncer_mini::new_debouncer; fn main() { std::thread::spawn(|| { diff --git a/examples/hot_reload_tide/Cargo.toml b/examples/hot_reload_tide/Cargo.toml index 44660b00..73ea87bc 100644 --- a/examples/hot_reload_tide/Cargo.toml +++ b/examples/hot_reload_tide/Cargo.toml @@ -11,4 +11,6 @@ tide = "0.16.0" async-std = { version = "1.6.0", features = ["attributes"] } serde_json = "1.0" serde = "1.0.115" -notify = { version = "5.0.0-pre.15", features = ["serde"] } +notify = { version = "5.0.0-pre.15", features = ["serde"], path = "../../notify" } + +[workspace] \ No newline at end of file diff --git a/examples/hot_reload_tide/tests/messages_test.rs b/examples/hot_reload_tide/tests/messages_test.rs index c7da197c..bed94127 100644 --- a/examples/hot_reload_tide/tests/messages_test.rs +++ b/examples/hot_reload_tide/tests/messages_test.rs @@ -1,4 +1,4 @@ -use hot_reload_tide::messages::{Config, load_config}; +use hot_reload_tide::messages::{load_config, Config}; #[test] fn load_config_from_file() { diff --git a/examples/watcher_kind/src/main.rs b/examples/watcher_kind.rs similarity index 95% rename from examples/watcher_kind/src/main.rs rename to examples/watcher_kind.rs index 1b0227ea..2e7b96cf 100644 --- a/examples/watcher_kind/src/main.rs +++ b/examples/watcher_kind.rs @@ -12,4 +12,5 @@ fn main() { } else { Box::new(RecommendedWatcher::new(tx).unwrap()) }; + // use _watcher here } diff --git a/examples/watcher_kind/Cargo.toml b/examples/watcher_kind/Cargo.toml deleted file mode 100644 index 3dd8ce04..00000000 --- a/examples/watcher_kind/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "watcher_kind" -version = "0.1.0" -authors = ["Aron Heinecke "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -notify = { version = "5.0.0-pre.15", path = "../../" } diff --git a/notify-debouncer-mini/Cargo.toml b/notify-debouncer-mini/Cargo.toml new file mode 100644 index 00000000..05768180 --- /dev/null +++ b/notify-debouncer-mini/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "notify-debouncer-mini" +version = "0.1.0" +edition = "2021" +description = "notify mini debouncer for events" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "notify_debouncer_mini" +path = "src/lib.rs" + +[dependencies] +notify = "5.0.0-pre.15" \ No newline at end of file diff --git a/src/debouncer.rs b/notify-debouncer-mini/src/lib.rs similarity index 98% rename from src/debouncer.rs rename to notify-debouncer-mini/src/lib.rs index 9ad648d3..3d0e6ebb 100644 --- a/src/debouncer.rs +++ b/notify-debouncer-mini/src/lib.rs @@ -11,7 +11,7 @@ use std::{ time::{Duration, Instant}, }; -use crate::{Error, ErrorKind, Event, RecommendedWatcher, Watcher}; +use notify::{Error, ErrorKind, Event, RecommendedWatcher, Watcher}; /// Deduplicate event data entry struct EventData { diff --git a/notify/.gitignore b/notify/.gitignore new file mode 100644 index 00000000..bf3bba40 --- /dev/null +++ b/notify/.gitignore @@ -0,0 +1,6 @@ +/target +/Cargo.lock +.*.sw* +tests/last-fails +tests/last-run.log +.cargo diff --git a/notify/Cargo.toml b/notify/Cargo.toml new file mode 100644 index 00000000..ddd9dce4 --- /dev/null +++ b/notify/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "notify" +version = "5.0.0-pre.15" +rust-version = "1.56" +description = "Cross-platform filesystem notification library" +documentation = "https://docs.rs/notify" +homepage = "https://github.com/notify-rs/notify" +repository = "https://github.com/notify-rs/notify.git" +readme = "README.md" +license = "CC0-1.0 OR Artistic-2.0" +keywords = ["events", "filesystem", "notify", "watch"] +categories = ["filesystem"] +authors = [ + "Félix Saparelli ", + "Daniel Faust " +] + +edition = "2021" + +[dependencies] +bitflags = "1.0.4" +crossbeam-channel = "0.5.0" +filetime = "0.2.6" +libc = "0.2.4" +serde = { version = "1.0.89", features = ["derive"], optional = true } +walkdir = "2.0.1" + +[target.'cfg(target_os="linux")'.dependencies] +inotify = { version = "0.9", default-features = false } +mio = { version = "0.8", features = ["os-ext"] } + +[target.'cfg(target_os="macos")'.dependencies] +fsevent-sys = { version = "4", optional = true } +kqueue = { version = "1.0", optional = true } +mio = { version = "0.8", features = ["os-ext"], optional = true } + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3.8", features = ["fileapi", "handleapi", "ioapiset", "minwinbase", "synchapi", "winbase", "winnt"] } + +[target.'cfg(any(target_os="freebsd", target_os="openbsd", target_os = "netbsd", target_os = "dragonflybsd"))'.dependencies] +kqueue = "^1.0.4" # fix for #344 +mio = { version = "0.8", features = ["os-ext"] } + +[dev-dependencies] +serde_json = "1.0.39" +tempfile = "3.2.0" +nix = "0.23.1" + +[features] +default = ["macos_fsevent"] +timing_tests = [] +manual_tests = [] +macos_kqueue = ["kqueue", "mio"] +macos_fsevent = ["fsevent-sys"] diff --git a/src/config.rs b/notify/src/config.rs similarity index 100% rename from src/config.rs rename to notify/src/config.rs diff --git a/src/error.rs b/notify/src/error.rs similarity index 100% rename from src/error.rs rename to notify/src/error.rs diff --git a/src/event.rs b/notify/src/event.rs similarity index 100% rename from src/event.rs rename to notify/src/event.rs diff --git a/src/fsevent.rs b/notify/src/fsevent.rs similarity index 100% rename from src/fsevent.rs rename to notify/src/fsevent.rs diff --git a/src/inotify.rs b/notify/src/inotify.rs similarity index 100% rename from src/inotify.rs rename to notify/src/inotify.rs diff --git a/src/kqueue.rs b/notify/src/kqueue.rs similarity index 100% rename from src/kqueue.rs rename to notify/src/kqueue.rs diff --git a/src/lib.rs b/notify/src/lib.rs similarity index 99% rename from src/lib.rs rename to notify/src/lib.rs index 0c45d51a..603adffa 100644 --- a/src/lib.rs +++ b/notify/src/lib.rs @@ -101,7 +101,6 @@ #![deny(missing_docs)] pub use config::{Config, RecursiveMode}; -pub use debouncer::new_debouncer; pub use error::{Error, ErrorKind, Result}; pub use event::{Event, EventKind}; use std::path::Path; @@ -143,7 +142,6 @@ pub mod null; pub mod poll; mod config; -mod debouncer; mod error; /// The set of requirements for watcher event handling functions. diff --git a/src/null.rs b/notify/src/null.rs similarity index 100% rename from src/null.rs rename to notify/src/null.rs diff --git a/src/poll.rs b/notify/src/poll.rs similarity index 99% rename from src/poll.rs rename to notify/src/poll.rs index f4cb6e61..400d33d5 100644 --- a/src/poll.rs +++ b/notify/src/poll.rs @@ -470,7 +470,6 @@ impl PollWatcher { { data_builder.update_timestamp(); - let vals = watches.values_mut(); for watch_data in vals { watch_data.rescan(&mut data_builder); diff --git a/src/windows.rs b/notify/src/windows.rs similarity index 100% rename from src/windows.rs rename to notify/src/windows.rs diff --git a/release_checklist.md b/release_checklist.md index 7b7d5cc0..339dc905 100644 --- a/release_checklist.md +++ b/release_checklist.md @@ -1,6 +1,10 @@ +# Release checklist of files to update -- update changelog -- update readme -- update lib.rs -- update cargo.toml +Specifically the notify version. + +- update CHANGELOG.md +- update README.md +- update notify/lib.rs +- update notify/cargo.toml examples/Cargo.toml examples/hot_reload_tide/Cargo.toml - bump version number on the root Cargo.toml and examples +- maybe update notify-debouncer-mini/Cargo.toml \ No newline at end of file From 7e19d2136a72ef2ba2d0d238e9a5299415f86a30 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Tue, 2 Aug 2022 21:50:33 +0200 Subject: [PATCH 7/8] allow more than channels and make crossbeam optional --- examples/debounced.rs | 7 +- examples/debounced_full_custom.rs | 31 ++++++ notify-debouncer-mini/Cargo.toml | 15 ++- notify-debouncer-mini/README.md | 10 ++ notify-debouncer-mini/src/lib.rs | 169 +++++++++++++++++++++++------- 5 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 examples/debounced_full_custom.rs create mode 100644 notify-debouncer-mini/README.md diff --git a/examples/debounced.rs b/examples/debounced.rs index c3192558..8e091e52 100644 --- a/examples/debounced.rs +++ b/examples/debounced.rs @@ -13,9 +13,12 @@ fn main() { } }); - let (rx, mut watcher) = new_debouncer(Duration::from_secs(2), None).unwrap(); + let (tx, rx) = std::sync::mpsc::channel(); - watcher + let mut debouncer = new_debouncer(Duration::from_secs(2), None, tx).unwrap(); + + debouncer + .watcher() .watch(Path::new("."), RecursiveMode::Recursive) .unwrap(); diff --git a/examples/debounced_full_custom.rs b/examples/debounced_full_custom.rs new file mode 100644 index 00000000..9aa79ef7 --- /dev/null +++ b/examples/debounced_full_custom.rs @@ -0,0 +1,31 @@ +use std::{path::Path, time::Duration}; + +use notify::{RecursiveMode, Watcher}; +use notify_debouncer_mini::new_debouncer; + +/// Debouncer with custom backend and waiting for exit +fn main() { + std::thread::spawn(|| { + let path = Path::new("test.txt"); + let _ = std::fs::remove_file(&path); + loop { + std::fs::write(&path, b"Lorem ipsum").unwrap(); + std::thread::sleep(Duration::from_millis(250)); + } + }); + + let (tx, rx) = std::sync::mpsc::channel(); + + let mut debouncer = new_debouncer_opt::<_,notify::PollWatcher>(Duration::from_secs(2), None, tx).unwrap(); + + debouncer + .watcher() + .watch(Path::new("."), RecursiveMode::Recursive) + .unwrap(); + + for events in rx { + for e in events { + println!("{:?}", e); + } + } +} diff --git a/notify-debouncer-mini/Cargo.toml b/notify-debouncer-mini/Cargo.toml index 05768180..f961beac 100644 --- a/notify-debouncer-mini/Cargo.toml +++ b/notify-debouncer-mini/Cargo.toml @@ -2,7 +2,15 @@ name = "notify-debouncer-mini" version = "0.1.0" edition = "2021" +rust-version = "1.56" description = "notify mini debouncer for events" +documentation = "https://docs.rs/notify_debouncer_mini" +homepage = "https://github.com/notify-rs/notify" +repository = "https://github.com/notify-rs/notify.git" +authors = ["Aron Heinecke "] +keywords = ["events", "filesystem", "notify", "watch"] +license = "CC0-1.0 OR Artistic-2.0" +readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,5 +18,10 @@ description = "notify mini debouncer for events" name = "notify_debouncer_mini" path = "src/lib.rs" +[features] +default = ["crossbeam-channel"] + [dependencies] -notify = "5.0.0-pre.15" \ No newline at end of file +notify = "5.0.0-pre.15" +crossbeam-channel = { version = "0.5", optional = true } +serde = { version = "1.0.89", features = ["derive"], optional = true } \ No newline at end of file diff --git a/notify-debouncer-mini/README.md b/notify-debouncer-mini/README.md new file mode 100644 index 00000000..f5bbfaa5 --- /dev/null +++ b/notify-debouncer-mini/README.md @@ -0,0 +1,10 @@ +# Notify debouncer + +Tiny debouncer for notify. Filters incoming events and emits only one event per timeframe per file. + +## Features + +- `crossbeam` enabled by default, for crossbeam channel support. +This may create problems used in tokio environments. See [#380](https://github.com/notify-rs/notify/issues/380). +Use someting like `notify-debouncer-mini = { version = "*", default-features = false }` to disable it. +- `serde` for serde support of event types, off by default \ No newline at end of file diff --git a/notify-debouncer-mini/src/lib.rs b/notify-debouncer-mini/src/lib.rs index 3d0e6ebb..ced8eeec 100644 --- a/notify-debouncer-mini/src/lib.rs +++ b/notify-debouncer-mini/src/lib.rs @@ -5,7 +5,7 @@ use std::{ collections::HashMap, path::PathBuf, sync::{ - mpsc::{self, Receiver}, + atomic::{AtomicBool, Ordering}, Arc, Mutex, }, time::{Duration, Instant}, @@ -13,6 +13,51 @@ use std::{ use notify::{Error, ErrorKind, Event, RecommendedWatcher, Watcher}; +/// The set of requirements for watcher debounce event handling functions. +/// +/// # Example implementation +/// +/// ```no_run +/// use notify::{Event, Result, EventHandler}; +/// +/// /// Prints received events +/// struct EventPrinter; +/// +/// impl EventHandler for EventPrinter { +/// fn handle_event(&mut self, event: Result) { +/// if let Ok(event) = event { +/// println!("Event: {:?}", event); +/// } +/// } +/// } +/// ``` +pub trait DebounceEventHandler: Send + 'static { + /// Handles an event. + fn handle_event(&mut self, event: DebouncedEvents); +} + +impl DebounceEventHandler for F +where + F: FnMut(DebouncedEvents) + Send + 'static, +{ + fn handle_event(&mut self, event: DebouncedEvents) { + (self)(event); + } +} + +#[cfg(feature = "crossbeam")] +impl DebounceEventHandler for crossbeam_channel::Sender { + fn handle_event(&mut self, event: DebouncedEvents) { + let _ = self.send(event); + } +} + +impl DebounceEventHandler for std::sync::mpsc::Sender { + fn handle_event(&mut self, event: DebouncedEvents) { + let _ = self.send(event); + } +} + /// Deduplicate event data entry struct EventData { /// Insertion Time @@ -31,7 +76,7 @@ impl EventData { } } -type DebounceChannelType = Result, Vec>; +type DebouncedEvents = Result, Vec>; /// A debounced event kind. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -118,17 +163,59 @@ impl DebounceDataInner { } } -/// Creates a new debounced watcher. -/// +/// Debouncer guard, stops the debouncer on drop +pub struct Debouncer { + stop: Arc, + watcher: T, + debouncer_thread: Option>, +} + +impl Debouncer { + /// Stop the debouncer, waits for the event thread to finish. + /// May block for the duration of one tick_rate. + pub fn stop(mut self) { + self.set_stop(); + if let Some(t) = self.debouncer_thread.take() { + let _ = t.join(); + } + } + + /// Stop the debouncer, does not wait for the event thread to finish. + pub fn stop_nonblocking(self) { + self.set_stop(); + } + + fn set_stop(&self) { + self.stop.store(true, Ordering::Relaxed); + } + + /// Access to the internally used notify Watcher backend + pub fn watcher(&mut self) -> &mut dyn Watcher { + &mut self.watcher + } +} + +impl Drop for Debouncer { + fn drop(&mut self) { + // don't imitate c++ async futures and block on drop + self.set_stop(); + } +} + +/// Creates a new debounced watcher with custom configuration. +/// /// Timeout is the amount of time after which a debounced event is emitted or a Continuous event is send, if there still are events incoming for the specific path. -/// +/// /// If tick_rate is None, notify will select a tick rate that is less than the provided timeout. -pub fn new_debouncer( +pub fn new_debouncer_opt( timeout: Duration, tick_rate: Option, -) -> Result<(Receiver, RecommendedWatcher), Error> { + mut event_handler: F, +) -> Result, Error> { let data = DebounceData::default(); + let stop = Arc::new(AtomicBool::new(false)); + let tick_div = 4; let tick = match tick_rate { Some(v) => { @@ -153,38 +240,31 @@ pub fn new_debouncer( data_w.timeout = timeout; } - let (tx, rx) = mpsc::channel(); - let data_c = data.clone(); - - std::thread::Builder::new() + let stop_c = stop.clone(); + let thread = std::thread::Builder::new() .name("notify-rs debouncer loop".to_string()) - .spawn(move || { - loop { - std::thread::sleep(tick); - let send_data; - let errors: Vec; - { - let mut lock = data_c.lock().expect("Can't lock debouncer data!"); - send_data = lock.debounced_events(); - errors = lock.errors(); - } - if send_data.len() > 0 { - // channel shut down - if tx.send(Ok(send_data)).is_err() { - break; - } - } - if errors.len() > 0 { - // channel shut down - if tx.send(Err(errors)).is_err() { - break; - } - } + .spawn(move || loop { + if stop_c.load(Ordering::Acquire) { + break; + } + std::thread::sleep(tick); + let send_data; + let errors: Vec; + { + let mut lock = data_c.lock().expect("Can't lock debouncer data!"); + send_data = lock.debounced_events(); + errors = lock.errors(); + } + if send_data.len() > 0 { + event_handler.handle_event(Ok(send_data)); + } + if errors.len() > 0 { + event_handler.handle_event(Err(errors)); } })?; - let watcher = RecommendedWatcher::new(move |e: Result| { + let watcher = T::new(move |e: Result| { let mut lock = data.lock().expect("Can't lock debouncer data!"); match e { @@ -194,5 +274,24 @@ pub fn new_debouncer( } })?; - Ok((rx, watcher)) + let guard = Debouncer { + watcher, + debouncer_thread: Some(thread), + stop, + }; + + Ok(guard) +} + +/// Short function to create a new debounced watcher with the recommended debouncer. +/// +/// Timeout is the amount of time after which a debounced event is emitted or a Continuous event is send, if there still are events incoming for the specific path. +/// +/// If tick_rate is None, notify will select a tick rate that is less than the provided timeout. +pub fn new_debouncer( + timeout: Duration, + tick_rate: Option, + event_handler: F, +) -> Result, Error> { + new_debouncer_opt::(timeout, tick_rate, event_handler) } From 00c7cba5fe9972b7d916de4909d298ce8f01b541 Mon Sep 17 00:00:00 2001 From: Aron Heinecke Date: Fri, 5 Aug 2022 00:21:25 +0200 Subject: [PATCH 8/8] add docs --- notify-debouncer-mini/README.md | 6 ++- notify-debouncer-mini/src/lib.rs | 84 +++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/notify-debouncer-mini/README.md b/notify-debouncer-mini/README.md index f5bbfaa5..5bdb7060 100644 --- a/notify-debouncer-mini/README.md +++ b/notify-debouncer-mini/README.md @@ -1,5 +1,7 @@ # Notify debouncer +[![» Docs](https://flat.badgen.net/badge/api/docs.rs/df3600)][docs] + Tiny debouncer for notify. Filters incoming events and emits only one event per timeframe per file. ## Features @@ -7,4 +9,6 @@ Tiny debouncer for notify. Filters incoming events and emits only one event per - `crossbeam` enabled by default, for crossbeam channel support. This may create problems used in tokio environments. See [#380](https://github.com/notify-rs/notify/issues/380). Use someting like `notify-debouncer-mini = { version = "*", default-features = false }` to disable it. -- `serde` for serde support of event types, off by default \ No newline at end of file +- `serde` for serde support of event types, off by default + +[docs]: https://docs.rs/notify/0.1/notify-debouncer-mini/ \ No newline at end of file diff --git a/notify-debouncer-mini/src/lib.rs b/notify-debouncer-mini/src/lib.rs index ced8eeec..bad3a54a 100644 --- a/notify-debouncer-mini/src/lib.rs +++ b/notify-debouncer-mini/src/lib.rs @@ -1,4 +1,43 @@ -//! Debouncer & access code +//! Debouncer for notify +//! +//! # Installation +//! +//! ```toml +//! [dependencies] +//! notify = "5.0.0-pre.15" +//! notify-debouncer-mini = "0.1" +//! ``` +//! +//! # Examples +//! +//! ```rust,no_run +//! # use std::path::Path; +//! # use std::time::Duration; +//! use notify::{Watcher, RecursiveMode, Result}; +//! use notify_debouncer_mini::{new_debouncer,DebounceEventResult}; +//! +//! # fn main() { +//! // Select recommended watcher for debouncer. +//! // Using a callback here, could also be a channel. +//! let mut debouncer = new_debouncer(Duration::from_secs(2), None, |res: DebounceEventResult| { +//! match res { +//! Ok(events) => events.iter().for_each(|e|println!("Event {:?} for {:?}",e.kind,e.path)), +//! Err(errors) => errors.iter().for_each(|e|println!("Error {:?}",e)), +//! } +//! }).unwrap(); +//! +//! // Add a path to be watched. All files and directories at that path and +//! // below will be monitored for changes. +//! debouncer.watcher().watch(Path::new("."), RecursiveMode::Recursive).unwrap(); +//! # } +//! ``` +//! +//! # Features +//! +//! The following feature can be turned on or off. +//! +//! - `crossbeam-channel` enabled by default, adds DebounceEventHandler support for crossbeam channels. +//! - `serde` enabled serde support for events. #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::{ @@ -17,43 +56,50 @@ use notify::{Error, ErrorKind, Event, RecommendedWatcher, Watcher}; /// /// # Example implementation /// -/// ```no_run -/// use notify::{Event, Result, EventHandler}; +/// ```rust,no_run +/// # use notify::{Event, Result, EventHandler}; +/// # use notify_debouncer_mini::{DebounceEventHandler,DebounceEventResult}; /// /// /// Prints received events /// struct EventPrinter; /// -/// impl EventHandler for EventPrinter { -/// fn handle_event(&mut self, event: Result) { -/// if let Ok(event) = event { -/// println!("Event: {:?}", event); +/// impl DebounceEventHandler for EventPrinter { +/// fn handle_event(&mut self, event: DebounceEventResult) { +/// match event { +/// Ok(events) => { +/// for event in events { +/// println!("Event {:?} for path {:?}",event.kind,event.path); +/// } +/// }, +/// // errors are batched, so you get either events or errors, probably both per debounce tick (two calls) +/// Err(errors) => errors.iter().for_each(|e|println!("Got error {:?}",e)), /// } /// } /// } /// ``` pub trait DebounceEventHandler: Send + 'static { /// Handles an event. - fn handle_event(&mut self, event: DebouncedEvents); + fn handle_event(&mut self, event: DebounceEventResult); } impl DebounceEventHandler for F where - F: FnMut(DebouncedEvents) + Send + 'static, + F: FnMut(DebounceEventResult) + Send + 'static, { - fn handle_event(&mut self, event: DebouncedEvents) { + fn handle_event(&mut self, event: DebounceEventResult) { (self)(event); } } #[cfg(feature = "crossbeam")] -impl DebounceEventHandler for crossbeam_channel::Sender { - fn handle_event(&mut self, event: DebouncedEvents) { +impl DebounceEventHandler for crossbeam_channel::Sender { + fn handle_event(&mut self, event: DebounceEventResult) { let _ = self.send(event); } } -impl DebounceEventHandler for std::sync::mpsc::Sender { - fn handle_event(&mut self, event: DebouncedEvents) { +impl DebounceEventHandler for std::sync::mpsc::Sender { + fn handle_event(&mut self, event: DebounceEventResult) { let _ = self.send(event); } } @@ -76,14 +122,16 @@ impl EventData { } } -type DebouncedEvents = Result, Vec>; +/// A result of debounced events. +/// Comes with either a vec of events or vec of errors. +pub type DebounceEventResult = Result, Vec>; /// A debounced event kind. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[non_exhaustive] pub enum DebouncedEventKind { - /// When precise events are disabled for files + /// No precise events Any, /// Event but debounce timed out (for example continuous writes) AnyContinuous, @@ -204,7 +252,7 @@ impl Drop for Debouncer { /// Creates a new debounced watcher with custom configuration. /// -/// Timeout is the amount of time after which a debounced event is emitted or a Continuous event is send, if there still are events incoming for the specific path. +/// Timeout is the amount of time after which a debounced event is emitted or a continuous event is send, if there still are events incoming for the specific path. /// /// If tick_rate is None, notify will select a tick rate that is less than the provided timeout. pub fn new_debouncer_opt( @@ -285,7 +333,7 @@ pub fn new_debouncer_opt( /// Short function to create a new debounced watcher with the recommended debouncer. /// -/// Timeout is the amount of time after which a debounced event is emitted or a Continuous event is send, if there still are events incoming for the specific path. +/// Timeout is the amount of time after which a debounced event is emitted or a continuous event is send, if there still are events incoming for the specific path. /// /// If tick_rate is None, notify will select a tick rate that is less than the provided timeout. pub fn new_debouncer(