#![deny(missing_docs)]
pub use config::{Config, RecursiveMode};
pub use error::{Error, ErrorKind, Result};
pub use notify_types::event::{self, Event, EventKind};
use std::path::Path;
pub(crate) type Receiver<T> = std::sync::mpsc::Receiver<T>;
pub(crate) type Sender<T> = std::sync::mpsc::Sender<T>;
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
pub(crate) type BoundSender<T> = std::sync::mpsc::SyncSender<T>;
#[inline]
pub(crate) fn unbounded<T>() -> (Sender<T>, Receiver<T>) {
std::sync::mpsc::channel()
}
#[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))]
#[inline]
pub(crate) fn bounded<T>(cap: usize) -> (BoundSender<T>, Receiver<T>) {
std::sync::mpsc::sync_channel(cap)
}
#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
pub use crate::fsevent::FsEventWatcher;
#[cfg(any(target_os = "linux", target_os = "android"))]
pub use crate::inotify::INotifyWatcher;
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "ios",
all(target_os = "macos", feature = "macos_kqueue")
))]
pub use crate::kqueue::KqueueWatcher;
pub use null::NullWatcher;
pub use poll::PollWatcher;
#[cfg(target_os = "windows")]
pub use windows::ReadDirectoryChangesWatcher;
#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
pub mod fsevent;
#[cfg(any(target_os = "linux", target_os = "android"))]
pub mod inotify;
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "ios",
all(target_os = "macos", feature = "macos_kqueue")
))]
pub mod kqueue;
#[cfg(target_os = "windows")]
pub mod windows;
pub mod null;
pub mod poll;
mod config;
mod error;
pub trait EventHandler: Send + 'static {
fn handle_event(&mut self, event: Result<Event>);
}
impl<F> EventHandler for F
where
F: FnMut(Result<Event>) + Send + 'static,
{
fn handle_event(&mut self, event: Result<Event>) {
(self)(event);
}
}
#[cfg(feature = "crossbeam-channel")]
impl EventHandler for crossbeam_channel::Sender<Result<Event>> {
fn handle_event(&mut self, event: Result<Event>) {
let _ = self.send(event);
}
}
#[cfg(feature = "flume")]
impl EventHandler for flume::Sender<Result<Event>> {
fn handle_event(&mut self, event: Result<Event>) {
let _ = self.send(event);
}
}
impl EventHandler for std::sync::mpsc::Sender<Result<Event>> {
fn handle_event(&mut self, event: Result<Event>) {
let _ = self.send(event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WatcherKind {
Inotify,
Fsevent,
Kqueue,
PollWatcher,
ReadDirectoryChangesWatcher,
NullWatcher,
}
pub trait PathsMut {
fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
fn remove(&mut self, path: &Path) -> Result<()>;
fn commit(self: Box<Self>) -> Result<()>;
}
pub trait Watcher {
fn new<F: EventHandler>(event_handler: F, config: config::Config) -> Result<Self>
where
Self: Sized;
fn watch(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()>;
fn unwatch(&mut self, path: &Path) -> Result<()>;
fn paths_mut<'me>(&'me mut self) -> Box<dyn PathsMut + 'me> {
struct DefaultPathsMut<'a, T: ?Sized>(&'a mut T);
impl<'a, T: Watcher + ?Sized> PathsMut for DefaultPathsMut<'a, T> {
fn add(&mut self, path: &Path, recursive_mode: RecursiveMode) -> Result<()> {
self.0.watch(path, recursive_mode)
}
fn remove(&mut self, path: &Path) -> Result<()> {
self.0.unwatch(path)
}
fn commit(self: Box<Self>) -> Result<()> {
Ok(())
}
}
Box::new(DefaultPathsMut(self))
}
fn configure(&mut self, _option: Config) -> Result<bool> {
Ok(false)
}
fn kind() -> WatcherKind
where
Self: Sized;
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub type RecommendedWatcher = INotifyWatcher;
#[cfg(all(target_os = "macos", not(feature = "macos_kqueue")))]
pub type RecommendedWatcher = FsEventWatcher;
#[cfg(target_os = "windows")]
pub type RecommendedWatcher = ReadDirectoryChangesWatcher;
#[cfg(any(
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "ios",
all(target_os = "macos", feature = "macos_kqueue")
))]
pub type RecommendedWatcher = KqueueWatcher;
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "windows",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "ios"
)))]
pub type RecommendedWatcher = PollWatcher;
pub fn recommended_watcher<F>(event_handler: F) -> Result<RecommendedWatcher>
where
F: EventHandler,
{
RecommendedWatcher::new(event_handler, Config::default())
}
#[cfg(test)]
mod tests {
use std::{
fs, iter,
time::{Duration, Instant},
};
use tempfile::tempdir;
use super::*;
#[test]
fn test_object_safe() {
let _watcher: &dyn Watcher = &NullWatcher;
}
#[test]
fn test_debug_impl() {
macro_rules! assert_debug_impl {
($t:ty) => {{
#[allow(dead_code)]
trait NeedsDebug: std::fmt::Debug {}
impl NeedsDebug for $t {}
}};
}
assert_debug_impl!(Config);
assert_debug_impl!(Error);
assert_debug_impl!(ErrorKind);
assert_debug_impl!(NullWatcher);
assert_debug_impl!(PollWatcher);
assert_debug_impl!(RecommendedWatcher);
assert_debug_impl!(RecursiveMode);
assert_debug_impl!(WatcherKind);
}
fn iter_with_timeout(rx: &Receiver<Result<Event>>) -> impl Iterator<Item = Event> + '_ {
let deadline = Instant::now() + Duration::from_secs(10);
iter::from_fn(move || {
if Instant::now() >= deadline {
return None;
}
Some(
rx.recv_timeout(deadline - Instant::now())
.expect("did not receive expected event")
.expect("received an error"),
)
})
}
#[test]
fn integration() -> std::result::Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(dir.path(), RecursiveMode::Recursive)?;
let file_path = dir.path().join("file.txt");
fs::write(&file_path, b"Lorem ipsum")?;
println!("waiting for event at {}", file_path.display());
for event in iter_with_timeout(&rx) {
if event.paths == vec![file_path.clone()]
|| event.paths == vec![file_path.canonicalize()?]
{
return Ok(());
}
println!("unexpected event: {event:?}");
}
panic!("did not receive expected event");
}
#[test]
#[cfg(target_os = "windows")]
fn test_windows_trash_dir() -> std::result::Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let child_dir = dir.path().join("child");
fs::create_dir(&child_dir)?;
let mut watcher = recommended_watcher(|_| {
})?;
watcher.watch(&child_dir, RecursiveMode::NonRecursive)?;
trash::delete(&child_dir)?;
watcher.watch(dir.path(), RecursiveMode::NonRecursive)?;
Ok(())
}
#[test]
fn test_paths_mut() -> std::result::Result<(), Box<dyn std::error::Error>> {
let dir = tempdir()?;
let dir_a = dir.path().join("a");
let dir_b = dir.path().join("b");
fs::create_dir(&dir_a)?;
fs::create_dir(&dir_b)?;
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
{
let mut watcher_paths = watcher.paths_mut();
watcher_paths.add(&dir_a, RecursiveMode::Recursive)?;
watcher_paths.add(&dir_b, RecursiveMode::Recursive)?;
watcher_paths.commit()?;
}
let a_file1 = dir_a.join("file1");
let b_file1 = dir_b.join("file1");
fs::write(&a_file1, b"Lorem ipsum")?;
fs::write(&b_file1, b"Lorem ipsum")?;
let mut a_file1_encountered: bool = false;
let mut b_file1_encountered: bool = false;
for event in iter_with_timeout(&rx) {
for path in event.paths {
a_file1_encountered =
a_file1_encountered || (path == a_file1 || path == a_file1.canonicalize()?);
b_file1_encountered =
b_file1_encountered || (path == b_file1 || path == b_file1.canonicalize()?);
}
if a_file1_encountered && b_file1_encountered {
break;
}
}
assert!(a_file1_encountered, "Did not receive event of {a_file1:?}");
assert!(b_file1_encountered, "Did not receive event of {b_file1:?}");
{
let mut watcher_paths = watcher.paths_mut();
watcher_paths.remove(&dir_a)?;
watcher_paths.commit()?;
}
let a_file2 = dir_a.join("file2");
let b_file2 = dir_b.join("file2");
fs::write(&a_file2, b"Lorem ipsum")?;
fs::write(&b_file2, b"Lorem ipsum")?;
for event in iter_with_timeout(&rx) {
for path in event.paths {
assert!(
path != a_file2 || path != a_file2.canonicalize()?,
"Event of {a_file2:?} should not be received"
);
if path == b_file2 || path == b_file2.canonicalize()? {
return Ok(());
}
}
}
panic!("Did not receive the event of {b_file2:?}");
}
}