commit 7a0967741d9604bd8f246c0f7af2f50f1f17e4f9 Author: pantonshire Date: Mon May 1 19:07:19 2023 +0100 🎉 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8be8d4b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mallard" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = "0.6.18" +hyper = { version = "0.14.26" } +notify = "5.1.0" +tera = "1.18.1" +tokio = { version = "1.28.0", features = ["full"] } +tracing = "0.1.37" +treacle = { git = "https://github.com/pantonshire/treacle.git" } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cbff609 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,294 @@ +use std::{ + path::Path, + time::Duration, + future::Future, + convert::Infallible, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, + sync::Arc, + thread::{JoinHandle, self}, + io, + fmt, + error, + panic, net::SocketAddr, +}; + +use axum::{Router, Server, Extension}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use tera::Tera; +use tokio::sync::RwLock as TokioRwLock; +use treacle::Debouncer; + +const DEFAULT_TEMPLATES_DEBOUNCE_TIME: Duration = Duration::from_millis(500); + +pub struct Mallard<'a, F> { + addr: SocketAddr, + router: Router, + templates: Option>, + templates_debounce_time: Duration, + shutdown_signal: Option, +} + +impl<'a> Mallard<'a, BottomFuture<()>> { + pub fn new(addr: SocketAddr, router: Router) -> Self { + Self { + addr, + router, + templates: None, + templates_debounce_time: DEFAULT_TEMPLATES_DEBOUNCE_TIME, + shutdown_signal: None, + } + } +} + +impl<'a, F> Mallard<'a, F> { + pub fn with_templates(self, dir: &'a str) -> Self { + // FIXME: escape glob characters first for `globwalk` + // (see https://git-scm.com/docs/gitignore#_pattern_format) + let glob = Path::new(dir) + .join("**/*") + .into_os_string() + .into_string() + .unwrap(); + + Self { + templates: Some(TemplatesDir { + watch_dir: dir, + glob, + }), + ..self + } + } + + pub fn with_templates_debounce_time(self, debounce_time: Duration) -> Self { + Self { + templates_debounce_time: debounce_time, + ..self + } + } + + pub fn with_shutdown_signal(self, signal: G) -> Mallard<'a, G> + where + G: Future, + { + Mallard { + addr: self.addr, + router: self.router, + templates: self.templates, + templates_debounce_time: self.templates_debounce_time, + shutdown_signal: Some(signal), + } + } + + pub fn init(self) -> Result, Error> { + let tera = match &self.templates { + Some(templates) => Tera::new(&templates.glob)?, + None => Tera::default(), + }; + + let ctx = Arc::new(MallardCtx { + tera: TokioRwLock::new(tera), + }); + + let reload_engine = match self.templates { + Some(templates) => { + // The debouncer is later moved into the event handler closure of the watcher, so + // it will be dropped and cleaned up when the watcher is dropped. + let (debouncer, debounced_rx) = Debouncer::new(self.templates_debounce_time)?; + + let watcher = { + let mut watcher = notify::recommended_watcher(move |res| match res { + Ok(_event) => { + debouncer.debounce_unit(); + }, + Err(err) => { + // FIXME: custom error handler + eprintln!("Filesystem event error: {}", err); + }, + })?; + + watcher.watch(templates.watch_dir.as_ref(), RecursiveMode::Recursive)?; + + watcher + }; + + let reloader = thread::Builder::new().spawn({ + let ctx = ctx.clone(); + + move || { + while let Ok(()) = debounced_rx.recv() { + // FIXME: tracing + + let reload_res = { + let mut guard = ctx.tera().blocking_write(); + guard.full_reload() + }; + + // FIXME: custom error handler + match reload_res { + Ok(()) => { + // println!("Reloaded templates"); + }, + Err(_) => { + // eprintln!("Error reloading templates: {}", err); + }, + } + } + + // println!("Stopping template reloader thread"); + } + })?; + + ReloadEngine { + watcher: Some(watcher), + reloader: Some(reloader), + } + }, + + None => { + ReloadEngine { + watcher: None, + reloader: None, + } + }, + }; + + Ok(InitialisedMallard { + addr: self.addr, + router: self.router, + ctx, + shutdown_signal: self.shutdown_signal, + reload_engine, + }) + } +} + +struct TemplatesDir<'a> { + watch_dir: &'a str, + glob: String, +} + +pub struct InitialisedMallard { + addr: SocketAddr, + router: Router, + ctx: Arc, + shutdown_signal: Option, + reload_engine: ReloadEngine, +} + +impl InitialisedMallard +where + F: Future, +{ + pub async fn run(self) -> Result<(), Error> { + let router = self.router + .layer(Extension(self.ctx)); + + let server = Server::try_bind(&self.addr)? + .serve(router.into_make_service()); + + match self.shutdown_signal { + Some(shutdown_signal) => { + server.with_graceful_shutdown(shutdown_signal).await + }, + None => { + server.await + }, + }?; + + // Drop the reload engine to stop the debouncer, watcher and reload thread. + // The drop is done explicitly for clarity. + drop(self.reload_engine); + + Ok(()) + } +} + +pub struct MallardCtx { + tera: TokioRwLock, +} + +impl MallardCtx { + pub fn tera(&self) -> &TokioRwLock { + &self.tera + } +} + +struct ReloadEngine { + watcher: Option, + reloader: Option>, +} + +impl Drop for ReloadEngine { + fn drop(&mut self) { + // Drop the watcher, which will drop the debouncer owned by its closure. Dropping the + // debouncer closes the associated mpsc channel, which causes the reloader thread to break + // out of its loop. + self.watcher.take(); + + // Now that the reloader thread will break out of its loop, we can join it and be sure that + // this will eventually terminate. + if let Some(Err(err)) = self.reloader.take().map(JoinHandle::join) { + panic::resume_unwind(err); + } + } +} + +struct BottomFuture { + bottom: Infallible, + phantom_data: PhantomData, +} + +impl Future for BottomFuture { + type Output = T; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + match self.bottom {} + } +} + +#[derive(Debug)] +pub enum Error { + Io(io::Error), + Tera(tera::Error), + Notify(notify::Error), + Hyper(hyper::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(err) => fmt::Display::fmt(err, f), + Self::Tera(err) => fmt::Display::fmt(err, f), + Self::Notify(err) => fmt::Display::fmt(err, f), + Self::Hyper(err) => fmt::Display::fmt(err, f), + } + } +} + +impl error::Error for Error {} + +impl From for Error { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +impl From for Error { + fn from(err: tera::Error) -> Self { + Self::Tera(err) + } +} + +impl From for Error { + fn from(err: notify::Error) -> Self { + Self::Notify(err) + } +} + +impl From for Error { + fn from(err: hyper::Error) -> Self { + Self::Hyper(err) + } +}