🎉 initial commit
commit
7a0967741d
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
@ -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" }
|
||||
@ -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<TemplatesDir<'a>>,
|
||||
templates_debounce_time: Duration,
|
||||
shutdown_signal: Option<F>,
|
||||
}
|
||||
|
||||
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<G>(self, signal: G) -> Mallard<'a, G>
|
||||
where
|
||||
G: Future<Output = ()>,
|
||||
{
|
||||
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<InitialisedMallard<F>, 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<F> {
|
||||
addr: SocketAddr,
|
||||
router: Router,
|
||||
ctx: Arc<MallardCtx>,
|
||||
shutdown_signal: Option<F>,
|
||||
reload_engine: ReloadEngine,
|
||||
}
|
||||
|
||||
impl<F> InitialisedMallard<F>
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
{
|
||||
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<Tera>,
|
||||
}
|
||||
|
||||
impl MallardCtx {
|
||||
pub fn tera(&self) -> &TokioRwLock<Tera> {
|
||||
&self.tera
|
||||
}
|
||||
}
|
||||
|
||||
struct ReloadEngine {
|
||||
watcher: Option<RecommendedWatcher>,
|
||||
reloader: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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<T> {
|
||||
bottom: Infallible,
|
||||
phantom_data: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Future for BottomFuture<T> {
|
||||
type Output = T;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
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<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tera::Error> for Error {
|
||||
fn from(err: tera::Error) -> Self {
|
||||
Self::Tera(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notify::Error> for Error {
|
||||
fn from(err: notify::Error) -> Self {
|
||||
Self::Notify(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(err: hyper::Error) -> Self {
|
||||
Self::Hyper(err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue