Skip to content

Commit

Permalink
Add blocking API
Browse files Browse the repository at this point in the history
  • Loading branch information
francesca64 committed Dec 3, 2019
1 parent 23c9008 commit b91f415
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 46 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Unreleased

# Version 0.4.3 (2019-12-03)

- Added `blocking` API.

# Version 0.4.2 (2019-06-12)

- Re-export `notify`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hotwatch"
version = "0.4.2"
version = "0.4.3"
authors = ["Francesca Plebani <[email protected]>"]
edition = "2018"
description = "A Rust library for conveniently watching and handling file changes."
Expand Down
21 changes: 21 additions & 0 deletions examples/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use hotwatch::{
blocking::{Flow, Hotwatch},
Event,
};
use std::path::Path;

fn main() -> Result<(), failure::Error> {
let mut watcher = Hotwatch::new()?;
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/data.json");
watcher.watch(&path, move |event| {
if let Event::Write(_path) = event {
Flow::Exit
} else {
Flow::Continue
}
})?;
println!("Edit data.json, and thou shalt be rewarded...");
watcher.run();
println!("🌭 🍔 🍟");
Ok(())
}
7 changes: 4 additions & 3 deletions examples/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ fn main() -> Result<(), failure::Error> {
let changed = AtomicBool::new(true).into();
{
let changed = Arc::clone(&changed);
watcher.watch(&path, move |event| match event {
Event::Write(_) => changed.store(true, Ordering::Release),
_ => (),
watcher.watch(&path, move |event| {
if let Event::Write(_path) = event {
changed.store(true, Ordering::Release);
}
})?;
}
loop {
Expand Down
157 changes: 157 additions & 0 deletions src/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Blocking file watching
use crate::{util, Error, Event};
use notify::Watcher as _;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::mpsc::{channel, Receiver},
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Flow {
/// Continue watching and blocking the thread.
Continue,
/// Stop watching, returning control of the thread.
Exit,
}

impl Default for Flow {
fn default() -> Self {
Self::Continue
}
}

/// A blocking hotwatch instance.
///
/// No watching will actually happen until you call [`Hotwatch::run`], which blocks
/// the thread until a handler returns [`Flow::Exit`]. This is useful if you just
/// want to wait on some criteria, rather than if you're building some long-running
/// sexy hot reload service.
///
/// Dropping this will unwatch everything.
pub struct Hotwatch {
watcher: notify::RecommendedWatcher,
handlers: HashMap<PathBuf, Box<dyn Fn(Event) -> Flow>>,
rx: Receiver<Event>,
}

impl std::fmt::Debug for Hotwatch {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct("Hotwatch").finish()
}
}

impl Hotwatch {
/// Creates a new blocking hotwatch instance.
///
/// # Errors
///
/// This will fail if the underlying [notify](https://docs.rs/notify/4.0/notify/)
/// instance fails to initialize.
///
/// # Examples
///
/// ```
/// use hotwatch::blocking::Hotwatch;
///
/// let hotwatch = Hotwatch::new().expect("hotwatch failed to initialize");
/// ```
pub fn new() -> Result<Self, Error> {
Self::new_with_custom_delay(std::time::Duration::from_secs(2))
}

/// Using [`Hotwatch::new`] will give you a default delay of 2 seconds.
/// This method allows you to specify your own value.
///
/// # Notes
///
/// A delay of over 30 seconds will prevent repetitions of previous events on macOS.
pub fn new_with_custom_delay(delay: std::time::Duration) -> Result<Self, Error> {
let (tx, rx) = channel();
let watcher = notify::Watcher::new(tx, delay).map_err(Error::Notify)?;
Ok(Self {
watcher,
handlers: Default::default(),
rx,
})
}

/// Watch a path and register a handler to it.
///
/// Handlers won't actually be run until you call [`Hotwatch::run`].
///
/// When watching a directory, that handler will receive all events for all directory
/// contents, even recursing through subdirectories.
///
/// Only the most specific applicable handler will be called. In other words, if you're
/// watching "dir" and "dir/file1", then only the latter handler will fire for changes to
/// `file1`.
///
/// # Errors
///
/// Watching will fail if the path can't be read, returning [`Error::Io`].
///
/// # Examples
///
/// ```
/// use hotwatch::{blocking::{Flow, Hotwatch}, Event};
///
/// let mut hotwatch = Hotwatch::new().expect("hotwatch failed to initialize!");
/// // Note that this won't actually do anything until you call `hotwatch.run()`!
/// hotwatch.watch("README.md", |event: Event| {
/// if let Event::Write(path) = event {
/// println!("{:?} changed!", path);
/// Flow::Exit
/// } else {
/// Flow::Continue
/// }
/// }).expect("failed to watch file!");
/// ```
pub fn watch<P, F>(&mut self, path: P, handler: F) -> Result<(), Error>
where
P: AsRef<Path>,
F: 'static + Fn(Event) -> Flow,
{
let absolute_path = path.as_ref().canonicalize()?;
self.watcher
.watch(&absolute_path, notify::RecursiveMode::Recursive)?;
self.handlers.insert(absolute_path, Box::new(handler));
Ok(())
}

/// Stop watching a path.
///
/// # Errors
///
/// This will fail if the path wasn't being watched, or if the path
/// couldn't be unwatched for some platform-specific internal reason.
pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
let absolute_path = path.as_ref().canonicalize()?;
self.watcher.unwatch(&absolute_path)?;
self.handlers.remove(&absolute_path);
Ok(())
}

/// Run handlers in an endless loop, blocking the thread.
///
/// The loop will only exit if a handler returns [`Flow::Exit`].
pub fn run(&mut self) {
loop {
match self.rx.recv() {
Ok(event) => {
util::log_event(&event);
if let Some(handler) = util::handler_for_event(&event, &self.handlers) {
if let Flow::Exit = handler(event) {
break;
}
}
}
Err(_) => {
util::log_dead();
break;
}
}
}
}
}
70 changes: 28 additions & 42 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
//! All handlers are run on that thread as well, so keep that in mind when attempting to access
//! outside data from within a handler.
//!
//! (There's also a [`blocking`] mode, in case you're a big fan of blocking.)
//!
//! Only the latest stable version of Rust is supported.
pub mod blocking;
mod util;

use notify::Watcher as _;
pub use notify::{self, DebouncedEvent as Event};
use std::{
collections::HashMap,
path::{Path, PathBuf},
Expand All @@ -17,22 +24,6 @@ use std::{
},
};

use notify::Watcher as _;
pub use notify::{self, DebouncedEvent as Event};

fn path_from_event(e: &Event) -> Option<PathBuf> {
match e {
Event::NoticeWrite(p)
| Event::NoticeRemove(p)
| Event::Create(p)
| Event::Write(p)
| Event::Chmod(p)
| Event::Remove(p)
| Event::Rename(p, _) => Some(p.clone()),
_ => None,
}
}

#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Expand All @@ -42,24 +33,24 @@ pub enum Error {
impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Io(error) => error.fmt(fmt),
Error::Notify(error) => error.fmt(fmt),
Self::Io(error) => error.fmt(fmt),
Self::Notify(error) => error.fmt(fmt),
}
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(error) => error.source(),
Error::Notify(error) => error.source(),
Self::Io(error) => error.source(),
Self::Notify(error) => error.source(),
}
}
}

impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
Self::Io(err)
}
}

Expand All @@ -68,14 +59,19 @@ impl From<notify::Error> for Error {
if let notify::Error::Io(err) = err {
err.into()
} else {
Error::Notify(err)
Self::Notify(err)
}
}
}

type Handler = Box<dyn Fn(Event) + Send>;
type HandlerMap = HashMap<PathBuf, Handler>;
type HandlerMap = HashMap<PathBuf, Box<dyn Fn(Event) + Send>>;

/// A non-blocking hotwatch instance.
///
/// Watching begins as soon as [`Self::watch`] is called, and occurs in a
/// background thread. The background thread runs until this is dropped.
///
/// Dropping this will also unwatch everything.
pub struct Hotwatch {
watcher: notify::RecommendedWatcher,
handlers: Arc<Mutex<HandlerMap>>,
Expand All @@ -88,7 +84,7 @@ impl std::fmt::Debug for Hotwatch {
}

impl Hotwatch {
/// Creates a new hotwatch instance.
/// Creates a new non-blocking hotwatch instance.
///
/// # Errors
///
Expand All @@ -115,10 +111,9 @@ impl Hotwatch {
pub fn new_with_custom_delay(delay: std::time::Duration) -> Result<Self, Error> {
let (tx, rx) = channel();
let handlers = Arc::<Mutex<_>>::default();
Hotwatch::run(Arc::clone(&handlers), rx);
notify::Watcher::new(tx, delay)
.map_err(Error::Notify)
.map(|watcher| Hotwatch { watcher, handlers })
Self::run(Arc::clone(&handlers), rx);
let watcher = notify::Watcher::new(tx, delay).map_err(Error::Notify)?;
Ok(Self { watcher, handlers })
}

/// Watch a path and register a handler to it.
Expand Down Expand Up @@ -180,23 +175,14 @@ impl Hotwatch {
std::thread::spawn(move || loop {
match rx.recv() {
Ok(event) => {
log::debug!("received event 🎉: {:#?}", event);
util::log_event(&event);
let handlers = handlers.lock().expect("handler mutex poisoned!");
if let Some(mut path) = path_from_event(&event) {
let mut handler = None;
let mut poppable = true;
while handler.is_none() && poppable {
log::debug!("matching against {:?}", path);
handler = handlers.get(&path);
poppable = path.pop();
}
if let Some(handler) = handler {
handler(event);
}
if let Some(handler) = util::handler_for_event(&event, &handlers) {
handler(event);
}
}
Err(_) => {
log::debug!("sender disconnected! the watcher is dead 💀");
util::log_dead();
break;
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::Event;
use std::{collections::HashMap, path::PathBuf};

pub fn log_event(e: &Event) {
log::debug!("received event 🎉: {:#?}", e);
}

pub fn log_dead() {
log::debug!("sender disconnected! the watcher is dead 💀");
}

pub fn handler_for_event<'a, H>(e: &Event, handlers: &'a HashMap<PathBuf, H>) -> Option<&'a H> {
fn path_from_event(e: &Event) -> Option<PathBuf> {
match e {
Event::NoticeWrite(p)
| Event::NoticeRemove(p)
| Event::Create(p)
| Event::Write(p)
| Event::Chmod(p)
| Event::Remove(p)
| Event::Rename(p, _) => Some(p.clone()),
_ => None,
}
}

fn find_handler<'a, H>(mut path: PathBuf, handlers: &'a HashMap<PathBuf, H>) -> Option<&'a H> {
let mut handler = None;
let mut poppable = true;
while handler.is_none() && poppable {
log::debug!("matching against {:?}", path);
handler = handlers.get(&path);
poppable = path.pop();
}
handler
}

path_from_event(e).and_then(|path| find_handler(path, handlers))
}

0 comments on commit b91f415

Please sign in to comment.