use std::{
fmt,
future::Future,
sync::{Arc, OnceLock},
};
use async_priority_channel as priority;
use atomic_take::AtomicTake;
use futures::TryFutureExt;
use miette::Diagnostic;
use tokio::{
spawn,
sync::{mpsc, Notify},
task::{JoinHandle, JoinSet},
};
use tracing::{debug, error, trace};
use watchexec_events::{Event, Priority};
use crate::{
action::{self, ActionHandler},
changeable::ChangeableFn,
error::{CriticalError, RuntimeError},
sources::{fs, keyboard, signal},
Config,
};
pub struct Watchexec {
pub config: Arc<Config>,
start_lock: Arc<Notify>,
event_input: priority::Sender<Event, Priority>,
handle: Arc<AtomicTake<JoinHandle<Result<(), CriticalError>>>>,
}
impl Default for Watchexec {
fn default() -> Self {
Self::with_config(Default::default()).expect("Use Watchexec::new() to avoid this panic")
}
}
impl fmt::Debug for Watchexec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Watchexec").finish_non_exhaustive()
}
}
impl Watchexec {
pub fn new(
action_handler: impl (Fn(ActionHandler) -> ActionHandler) + Send + Sync + 'static,
) -> Result<Arc<Self>, CriticalError> {
let config = Config::default();
config.on_action(action_handler);
Self::with_config(config).map(Arc::new)
}
pub fn new_async(
action_handler: impl (Fn(ActionHandler) -> Box<dyn Future<Output = ActionHandler> + Send + Sync>)
+ Send
+ Sync
+ 'static,
) -> Result<Arc<Self>, CriticalError> {
let config = Config::default();
config.on_action_async(action_handler);
Self::with_config(config).map(Arc::new)
}
pub fn with_config(config: Config) -> Result<Self, CriticalError> {
debug!(?config, pid=%std::process::id(), version=%env!("CARGO_PKG_VERSION"), "initialising");
let config = Arc::new(config);
let outer_config = config.clone();
let notify = Arc::new(Notify::new());
let start_lock = notify.clone();
let (ev_s, ev_r) =
priority::bounded(config.event_channel_size.try_into().unwrap_or(u64::MAX));
let event_input = ev_s.clone();
trace!("creating main task");
let handle = spawn(async move {
trace!("waiting for start lock");
notify.notified().await;
debug!("starting main task");
let (er_s, er_r) = mpsc::channel(config.error_channel_size);
let mut tasks = JoinSet::new();
tasks.spawn(action::worker(config.clone(), er_s.clone(), ev_r).map_ok(|()| "action"));
tasks.spawn(fs::worker(config.clone(), er_s.clone(), ev_s.clone()).map_ok(|()| "fs"));
tasks.spawn(
signal::worker(config.clone(), er_s.clone(), ev_s.clone()).map_ok(|()| "signal"),
);
tasks.spawn(
keyboard::worker(config.clone(), er_s.clone(), ev_s.clone())
.map_ok(|()| "keyboard"),
);
tasks.spawn(error_hook(er_r, config.error_handler.clone()).map_ok(|()| "error"));
while let Some(Ok(res)) = tasks.join_next().await {
match res {
Ok("action") => {
debug!("action worker exited, ending watchexec");
break;
}
Ok(task) => {
debug!(task, "worker exited");
}
Err(CriticalError::Exit) => {
trace!("got graceful exit request via critical error, erasing the error");
ev_s.close();
}
Err(e) => {
return Err(e);
}
}
}
debug!("main task graceful exit");
tasks.shutdown().await;
Ok(())
});
trace!("done with setup");
Ok(Self {
config: outer_config,
start_lock,
event_input,
handle: Arc::new(AtomicTake::new(handle)),
})
}
pub async fn send_event(&self, event: Event, priority: Priority) -> Result<(), CriticalError> {
self.event_input.send(event, priority).await?;
Ok(())
}
pub fn main(&self) -> JoinHandle<Result<(), CriticalError>> {
trace!("notifying start lock");
self.start_lock.notify_one();
debug!("handing over main task handle");
self.handle
.take()
.expect("Watchexec::main was called twice")
}
}
async fn error_hook(
mut errors: mpsc::Receiver<RuntimeError>,
handler: ChangeableFn<ErrorHook, ()>,
) -> Result<(), CriticalError> {
while let Some(err) = errors.recv().await {
if matches!(err, RuntimeError::Exit) {
trace!("got graceful exit request via runtime error, upgrading to crit");
return Err(CriticalError::Exit);
}
error!(%err, "runtime error");
let payload = ErrorHook::new(err);
let crit = payload.critical.clone();
handler.call(payload);
ErrorHook::handle_crit(crit)?;
}
Ok(())
}
#[derive(Debug)]
pub struct ErrorHook {
pub error: RuntimeError,
critical: Arc<OnceLock<CriticalError>>,
}
impl ErrorHook {
fn new(error: RuntimeError) -> Self {
Self {
error,
critical: Default::default(),
}
}
fn handle_crit(crit: Arc<OnceLock<CriticalError>>) -> Result<(), CriticalError> {
match Arc::try_unwrap(crit) {
Err(err) => {
error!(?err, "error handler hook has an outstanding ref");
Ok(())
}
Ok(crit) => crit.into_inner().map_or_else(
|| Ok(()),
|crit| {
debug!(%crit, "error handler output a critical error");
Err(crit)
},
),
}
}
pub fn critical(self, critical: CriticalError) {
self.critical.set(critical).ok();
}
pub fn elevate(self) {
let Self { error, critical } = self;
critical
.set(CriticalError::Elevated {
help: error.help().map(|h| h.to_string()),
err: error,
})
.ok();
}
}