Skip to content

Commit d824023

Browse files
authored
feat: introduce Watcher::paths_mut for adding/removing paths in batch (#692)
1 parent b984134 commit d824023

File tree

2 files changed

+170
-10
lines changed

2 files changed

+170
-10
lines changed

notify/src/fsevent.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
#![allow(non_upper_case_globals, dead_code)]
1616

1717
use crate::event::*;
18-
use crate::{unbounded, Config, Error, EventHandler, RecursiveMode, Result, Sender, Watcher};
18+
use crate::{
19+
unbounded, Config, Error, EventHandler, PathsMut, RecursiveMode, Result, Sender, Watcher,
20+
};
1921
use fsevent_sys as fs;
2022
use fsevent_sys::core_foundation as cf;
2123
use std::collections::HashMap;
@@ -265,6 +267,29 @@ extern "C" {
265267
fn CFRunLoopIsWaiting(runloop: cf::CFRunLoopRef) -> cf::Boolean;
266268
}
267269

270+
struct FsEventPathsMut<'a>(&'a mut FsEventWatcher);
271+
impl<'a> FsEventPathsMut<'a> {
272+
fn new(watcher: &'a mut FsEventWatcher) -> Self {
273+
watcher.stop();
274+
Self(watcher)
275+
}
276+
}
277+
impl PathsMut for FsEventPathsMut<'_> {
278+
fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> {
279+
self.0.append_path(path, recursive_mode)
280+
}
281+
282+
fn remove(&mut self, path: &Path) -> Result<()> {
283+
self.0.remove_path(path)
284+
}
285+
286+
fn commit(&mut self) -> Result<()> {
287+
// ignore return error: may be empty path list
288+
let _ = self.0.run();
289+
Ok(())
290+
}
291+
}
292+
268293
impl FsEventWatcher {
269294
fn from_event_handler(event_handler: Arc<Mutex<dyn EventHandler>>) -> Result<Self> {
270295
Ok(FsEventWatcher {
@@ -561,6 +586,10 @@ impl Watcher for FsEventWatcher {
561586
self.watch_inner(path, recursive_mode)
562587
}
563588

589+
fn paths_mut<'me>(&'me mut self) -> Box<dyn PathsMut + 'me> {
590+
Box::new(FsEventPathsMut::new(self))
591+
}
592+
564593
fn unwatch(&mut self, path: &Path) -> Result<()> {
565594
self.unwatch_inner(path)
566595
}

notify/src/lib.rs

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,20 @@ pub enum WatcherKind {
293293
NullWatcher,
294294
}
295295

296+
/// Providing methods for adding and removing paths to watch.
297+
///
298+
/// `Box<dyn PathsMut>` is created by [`Watcher::paths_mut`]. See its documentation for more.
299+
pub trait PathsMut {
300+
/// Add a new path to watch. See [`Watcher::watch`] for more.
301+
fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
302+
303+
/// Remove a path from watching. See [`Watcher::unwatch`] for more.
304+
fn remove(&mut self, path: &Path) -> Result<()>;
305+
306+
/// Ensure previously added/removed paths are applied.
307+
fn commit(&mut self) -> Result<()>;
308+
}
309+
296310
/// Type that can deliver file activity notifications
297311
///
298312
/// `Watcher` is implemented per platform using the best implementation available on that platform.
@@ -328,6 +342,42 @@ pub trait Watcher {
328342
/// fails.
329343
fn unwatch(&mut self, path: &Path) -> Result<()>;
330344

345+
/// Add/remove paths to watch.
346+
///
347+
/// For some watcher implementations this method provides better performance than multiple calls to [`Watcher::watch`] and [`Watcher::unwatch`] if you want to add/remove many paths at once.
348+
///
349+
/// # Examples
350+
///
351+
/// ```
352+
/// # use notify::{Watcher, RecursiveMode, Result};
353+
/// # use std::path::Path;
354+
/// # fn main() -> Result<()> {
355+
/// # let many_paths_to_add = vec![];
356+
/// let mut watcher = notify::recommended_watcher(|_event| { /* event handler */ })?;
357+
/// let mut watcher_paths = watcher.paths_mut();
358+
/// for path in many_paths_to_add {
359+
/// watcher_paths.add(path, RecursiveMode::Recursive)?;
360+
/// }
361+
/// watcher_paths.commit()?;
362+
/// # Ok(())
363+
/// # }
364+
/// ```
365+
fn paths_mut<'me>(&'me mut self) -> Box<dyn PathsMut + 'me> {
366+
struct DefaultPathsMut<'a, T: ?Sized>(&'a mut T);
367+
impl<'a, T: Watcher + ?Sized> PathsMut for DefaultPathsMut<'a, T> {
368+
fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> {
369+
self.0.watch(path, recursive_mode)
370+
}
371+
fn remove(&mut self, path: &Path) -> Result<()> {
372+
self.0.unwatch(path)
373+
}
374+
fn commit(&mut self) -> Result<()> {
375+
Ok(())
376+
}
377+
}
378+
Box::new(DefaultPathsMut(self))
379+
}
380+
331381
/// Configure the watcher at runtime.
332382
///
333383
/// See the [`Config`](config/struct.Config.html) struct for all configuration options.
@@ -392,7 +442,7 @@ where
392442
#[cfg(test)]
393443
mod tests {
394444
use std::{
395-
fs,
445+
fs, iter,
396446
time::{Duration, Instant},
397447
};
398448

@@ -425,6 +475,21 @@ mod tests {
425475
assert_debug_impl!(WatcherKind);
426476
}
427477

478+
fn iter_with_timeout(rx: &Receiver<Result<Event>>) -> impl Iterator<Item = Event> + '_ {
479+
// wait for up to 10 seconds for the events
480+
let deadline = Instant::now() + Duration::from_secs(10);
481+
iter::from_fn(move || {
482+
if Instant::now() >= deadline {
483+
return None;
484+
}
485+
Some(
486+
rx.recv_timeout(deadline - Instant::now())
487+
.expect("did not receive expected event")
488+
.expect("received an error"),
489+
)
490+
})
491+
}
492+
428493
#[test]
429494
fn integration() -> std::result::Result<(), Box<dyn std::error::Error>> {
430495
let dir = tempdir()?;
@@ -440,14 +505,8 @@ mod tests {
440505

441506
println!("waiting for event at {}", file_path.display());
442507

443-
// wait for up to 10 seconds for the create event, ignore all other events
444-
let deadline = Instant::now() + Duration::from_secs(10);
445-
while deadline > Instant::now() {
446-
let event = rx
447-
.recv_timeout(deadline - Instant::now())
448-
.expect("did not receive expected event")
449-
.expect("received an error");
450-
508+
// wait for the create event, ignore all other events
509+
for event in iter_with_timeout(&rx) {
451510
if event.paths == vec![file_path.clone()]
452511
|| event.paths == vec![file_path.canonicalize()?]
453512
{
@@ -478,4 +537,76 @@ mod tests {
478537

479538
Ok(())
480539
}
540+
541+
#[test]
542+
fn test_paths_mut() -> std::result::Result<(), Box<dyn std::error::Error>> {
543+
let dir = tempdir()?;
544+
545+
let dir_a = dir.path().join("a");
546+
let dir_b = dir.path().join("b");
547+
548+
fs::create_dir(&dir_a)?;
549+
fs::create_dir(&dir_b)?;
550+
551+
let (tx, rx) = std::sync::mpsc::channel();
552+
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
553+
554+
// start watching a and b
555+
{
556+
let mut watcher_paths = watcher.paths_mut();
557+
watcher_paths.add(&dir_a, RecursiveMode::Recursive)?;
558+
watcher_paths.add(&dir_b, RecursiveMode::Recursive)?;
559+
watcher_paths.commit()?;
560+
}
561+
562+
// create file1 in both a and b
563+
let a_file1 = dir_a.join("file1");
564+
let b_file1 = dir_b.join("file1");
565+
fs::write(&a_file1, b"Lorem ipsum")?;
566+
fs::write(&b_file1, b"Lorem ipsum")?;
567+
568+
// wait for create events of a/file1 and b/file1
569+
let mut a_file1_encountered: bool = false;
570+
let mut b_file1_encountered: bool = false;
571+
for event in iter_with_timeout(&rx) {
572+
for path in event.paths {
573+
a_file1_encountered =
574+
a_file1_encountered || (path == a_file1 || path == a_file1.canonicalize()?);
575+
b_file1_encountered =
576+
b_file1_encountered || (path == b_file1 || path == b_file1.canonicalize()?);
577+
}
578+
if a_file1_encountered && b_file1_encountered {
579+
break;
580+
}
581+
}
582+
assert!(a_file1_encountered, "Did not receive event of {a_file1:?}");
583+
assert!(b_file1_encountered, "Did not receive event of {b_file1:?}");
584+
585+
// stop watching a
586+
{
587+
let mut watcher_paths = watcher.paths_mut();
588+
watcher_paths.remove(&dir_a)?;
589+
watcher_paths.commit()?;
590+
}
591+
592+
// create file2 in both a and b
593+
let a_file2 = dir_a.join("file2");
594+
let b_file2 = dir_b.join("file2");
595+
fs::write(&a_file2, b"Lorem ipsum")?;
596+
fs::write(&b_file2, b"Lorem ipsum")?;
597+
598+
// wait for the create event of b/file2 only
599+
for event in iter_with_timeout(&rx) {
600+
for path in event.paths {
601+
assert!(
602+
path != a_file2 || path != a_file2.canonicalize()?,
603+
"Event of {a_file2:?} should not be received"
604+
);
605+
if path == b_file2 || path == b_file2.canonicalize()? {
606+
return Ok(());
607+
}
608+
}
609+
}
610+
panic!("Did not receive the event of {b_file2:?}");
611+
}
481612
}

0 commit comments

Comments
 (0)