diff --git a/lib/src/command.rs b/lib/src/command.rs index 294526de..8cc7bf5a 100644 --- a/lib/src/command.rs +++ b/lib/src/command.rs @@ -8,7 +8,7 @@ use tokio::process::Command; /// conventions. Also `Cmd` is only available in Windows, while `Powershell` is /// also available on unices (provided the end-user has it installed, of course). /// -/// See [`Config.cmd`][crate::config::Config] for the semantics of `None` vs the +/// See [`Config.cmd`] for the semantics of `None` vs the /// other options. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Shell { @@ -55,7 +55,7 @@ impl Default for Shell { } impl Shell { - /// Obtain a [`Command`] given the cmd vec from [`Config`][crate::config::Config]. + /// Obtain a [`Command`] given a list of command parts. /// /// Behaves as described in the enum documentation. /// diff --git a/lib/src/config.rs b/lib/src/config.rs index a463b35d..7567411b 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -1,15 +1,19 @@ +use std::fmt; + use derive_builder::Builder; -/// Configuration for [`Watchexec`][crate::Watchexec]. +use crate::{error::RuntimeError, handler::Handler}; + +/// Runtime configuration for [`Watchexec`][crate::Watchexec]. /// -/// This is used both for constructing the instance and to reconfigure it at runtime, though note -/// that some fields are only applied at construction time. +/// This is used both when constructing the instance (as initial configuration) and to reconfigure +/// it at runtime via [`Watchexec::reconfig()`][crate::Watchexec::reconfig()]. /// -/// Use [`ConfigBuilder`] to build a new one, or modify an existing one. This struct is marked -/// non-exhaustive such that new options may be added without breaking change. +/// Use [`RuntimeConfigBuilder`] to build a new one, or modify an existing one. This struct is +/// marked non-exhaustive such that new options may be added without breaking change. #[derive(Builder, Clone, Debug)] #[non_exhaustive] -pub struct Config { +pub struct RuntimeConfig { /// Working data for the filesystem event source. /// /// This notably includes the path set to be watched. @@ -22,13 +26,35 @@ pub struct Config { /// filtering, etc. #[builder(default)] pub action: crate::action::WorkingData, +} + +/// Initialisation configuration for [`Watchexec`][crate::Watchexec]. +/// +/// This is used only for constructing the instance. +/// +/// Use [`InitConfigBuilder`] to build a new one, or modify an existing one. This struct is marked +/// non-exhaustive such that new options may be added without breaking change. Note that this +/// builder uses a different style (consuming `self`) for technical reasons (cannot be `Clone`d). +#[derive(Builder)] +#[builder(pattern = "owned")] +#[non_exhaustive] +pub struct InitConfig { + /// Runtime error handler. + /// + /// This is run on every runtime error that occurs within watchexec. By default the placeholder + /// `()` handler is used, which discards all errors. + /// + /// If the handler errors, [_that_ error][crate::error::RuntimeError::Handler] is immediately + /// given to the handler. If that second handler call errors as well, its error is ignored. + /// + /// Only used at construction time, cannot be changed via reconfiguration. + #[builder(default = "Box::new(()) as _")] + pub error_handler: Box + Send>, /// Internal: the buffer size of the channel which carries runtime errors. /// /// The default (64) is usually fine. If you expect a much larger throughput of runtime errors, - /// adjusting this value may help. (Fixing whatever is causing the errors may also help.) - /// - /// Only used at construction time, cannot be changed via reconfiguration. + /// or if your `error_handler` is slow, adjusting this value may help. #[builder(default = "64")] pub error_channel_size: usize, @@ -36,8 +62,15 @@ pub struct Config { /// /// The default (1024) is usually fine. If you expect a much larger throughput of events, /// adjusting this value may help. - /// - /// Only used at construction time, cannot be changed via reconfiguration. #[builder(default = "1024")] pub event_channel_size: usize, } + +impl fmt::Debug for InitConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InitConfig") + .field("error_channel_size", &self.error_channel_size) + .field("event_channel_size", &self.event_channel_size) + .finish_non_exhaustive() + } +} diff --git a/lib/src/handler.rs b/lib/src/handler.rs index e3306bc7..5e64a7bd 100644 --- a/lib/src/handler.rs +++ b/lib/src/handler.rs @@ -4,7 +4,8 @@ //! - for closures, //! - for std and tokio channels, //! - for printing to writers, in `Debug` and `Display` (where supported) modes (generally used for -//! debugging and testing, as they don't allow any other output customisation). +//! debugging and testing, as they don't allow any other output customisation), +//! - for `()`, as placeholder. //! //! The implementation for [`FnMut`] only supports fns that return a [`Future`]. Unfortunately //! it's not possible to provide an implementation for fns that don't return a `Future` as well, @@ -86,7 +87,6 @@ use std::{error::Error, future::Future, io::Write, marker::PhantomData}; use tokio::runtime::Handle; -use tracing::{event, Level}; /// A callable that can be used to hook into watchexec. pub trait Handler { @@ -160,6 +160,12 @@ where } } +impl Handler for () { + fn handle(&mut self, _data: T) -> Result<(), Box> { + Ok(()).map_err(|e: std::convert::Infallible| Box::new(e) as _) + } +} + impl Handler for std::sync::mpsc::Sender where T: Send + 'static, diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c9b5c2ba..0ff7e7b2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -6,22 +6,26 @@ //! This library is powered by [Tokio](https://tokio.rs), minimum version 1.10. This requirement may //! change (upwards) in the future without breaking change. //! -//! The main way to use this crate involves constructing a [`Watchexec`] around a [`Config`] and -//! running it. The config may contain some instances of [`Handler`][handler::Handler]s, hooking -//! into watchexec at various points. +//! The main way to use this crate involves constructing a [`Watchexec`] around an +//! [`InitConfig`][config::InitConfig] and a [`RuntimeConfig`][config::RuntimeConfig], then running +//! it. [`Handler`][handler::Handler]s are used to hook into watchexec at various points. The +//! runtime config can be changed at any time with the [`reconfig()`][Watchexec::reconfig()] method. //! //! ```ignore // TODO: implement and switch to no_run -//! use watchexec::{Watchexec, ConfigBuilder, Handler as _}; +//! use watchexec::{Watchexec, InitConfigBuilder, RuntimeConfigBuilder, Handler as _}; //! //! #[tokio::main] //! async fn main() { -//! let mut config = ConfigBuilder::new() +//! let init = InitConfigBuilder::new() +//! .error_handler(PrintDebug(std::io::stderr())); +//! +//! let mut runtime = RuntimeConfigBuilder::new() //! config.pathset(["watchexec.conf"]); //! //! let conf = YourConfigFormat::load_from_file("watchexec.conf").await?; -//! conf.apply(&mut config); +//! conf.apply(&mut runtime); //! -//! let we = Watchexec::new(config.build().unwrap()).unwrap(); +//! let we = Watchexec::new(init.build().unwrap(), runtime.build().unwrap()).unwrap(); //! let w = we.clone(); //! //! let c = config.clone(); @@ -29,8 +33,8 @@ //! if e.path().map(|p| p.ends_with("watchexec.conf")).unwrap_or(false) { //! let conf = YourConfigFormat::load_from_file("watchexec.conf").await?; //! -//! conf.apply(&mut config); -//! w.reconfigure(config.build()); +//! conf.apply(&mut runtime); +//! w.reconfigure(runtime.build().unwrap()); //! // tada! self-reconfiguring watchexec on config file change! //! } //! }); @@ -62,12 +66,10 @@ pub mod handler; pub mod signal; // the core experience -mod config; +pub mod config; mod watchexec; #[doc(inline)] pub use crate::watchexec::Watchexec; -#[doc(inline)] -pub use config::{Config, ConfigBuilder}; // the *action* is debounced, not the events diff --git a/lib/src/watchexec.rs b/lib/src/watchexec.rs index 514c4abf..7657cb77 100644 --- a/lib/src/watchexec.rs +++ b/lib/src/watchexec.rs @@ -1,4 +1,8 @@ -use std::{mem::take, sync::Arc}; +use std::{ + fmt, + mem::{replace, take}, + sync::Arc, +}; use atomic_take::AtomicTake; use futures::FutureExt; @@ -11,35 +15,48 @@ use tokio::{ use crate::{ action, - config::Config, - error::{CriticalError, ReconfigError}, - fs, signal, + config::{InitConfig, RuntimeConfig}, + error::{CriticalError, ReconfigError, RuntimeError}, + fs, + handler::Handler, + signal, }; -#[derive(Debug)] pub struct Watchexec { handle: Arc>>>, start_lock: Arc, + action_watch: watch::Sender, fs_watch: watch::Sender, } +impl fmt::Debug for Watchexec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Watchexec").finish_non_exhaustive() + } +} + impl Watchexec { /// TODO /// /// Returns an [`Arc`] for convenience; use [`try_unwrap`][Arc::try_unwrap()] to get the value /// directly if needed. - pub fn new(mut config: Config) -> Result, CriticalError> { - let (fs_s, fs_r) = watch::channel(take(&mut config.fs)); - let (ac_s, ac_r) = watch::channel(take(&mut config.action)); + pub fn new( + mut init: InitConfig, + mut runtime: RuntimeConfig, + ) -> Result, CriticalError> { + let (fs_s, fs_r) = watch::channel(take(&mut runtime.fs)); + let (ac_s, ac_r) = watch::channel(take(&mut runtime.action)); let notify = Arc::new(Notify::new()); let start_lock = notify.clone(); let handle = spawn(async move { notify.notified().await; - let (er_s, er_r) = mpsc::channel(config.error_channel_size); - let (ev_s, ev_r) = mpsc::channel(config.event_channel_size); + let (er_s, er_r) = mpsc::channel(init.error_channel_size); + let (ev_s, ev_r) = mpsc::channel(init.event_channel_size); + + let eh = replace(&mut init.error_handler, Box::new(()) as _); macro_rules! subtask { ($task:expr) => { @@ -51,18 +68,21 @@ impl Watchexec { let fs = subtask!(fs::worker(fs_r, er_s.clone(), ev_s.clone())); let signal = subtask!(signal::worker(er_s.clone(), ev_s.clone())); + let error_hook = subtask!(error_hook(er_r, eh)); + try_join!(action, fs, signal).map(drop) }); Ok(Arc::new(Self { handle: Arc::new(AtomicTake::new(handle)), start_lock, + action_watch: ac_s, fs_watch: fs_s, })) } - pub fn reconfig(&self, config: Config) -> Result<(), ReconfigError> { + pub fn reconfig(&self, config: RuntimeConfig) -> Result<(), ReconfigError> { self.action_watch.send(config.action)?; self.fs_watch.send(config.fs)?; Ok(()) @@ -88,3 +108,21 @@ fn flatten(join_res: Result, JoinError>) -> Result<(), .map_err(CriticalError::MainTaskJoin) .and_then(|x| x) } + +async fn error_hook( + mut errors: mpsc::Receiver, + mut handler: Box + Send>, +) -> Result<(), CriticalError> { + while let Some(err) = errors.recv().await { + if let Err(e) = handler.handle(err) { + handler + .handle(RuntimeError::Handler { + ctx: "error hook", + err: e.to_string(), + }) + .ok(); + } + } + + Ok(()) +}