From 2052092796ce61f03d5dca2de840c6337c431919 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Tue, 14 May 2024 12:28:27 -0500 Subject: [PATCH] Progress on modularizing watchers --- src/file_watcher.rs | 42 ++++++++++++++++++++ src/live_reload.rs | 5 +++ src/main.rs | 95 +++++++++++---------------------------------- src/state.rs | 16 ++++++++ src/static_files.rs | 30 ++++++++++++++ src/templates.rs | 59 ++++++++++++++++++++++------ 6 files changed, 163 insertions(+), 84 deletions(-) create mode 100644 src/file_watcher.rs create mode 100644 src/live_reload.rs create mode 100644 src/state.rs create mode 100644 src/static_files.rs diff --git a/src/file_watcher.rs b/src/file_watcher.rs new file mode 100644 index 0000000..6e16046 --- /dev/null +++ b/src/file_watcher.rs @@ -0,0 +1,42 @@ +use std::path::Path; + +use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; +use tokio::sync::mpsc::channel; +use tracing::{error, info, instrument}; + +use crate::Besult; + +pub type FileWatcher = RecommendedWatcher; + +pub mod prelude { + pub use super::{file_watcher, FileWatcher}; +} + +pub fn file_watcher(dir: P, cb: F) -> Besult +where + P: AsRef, + F: Fn(Event) -> () + std::marker::Send + 'static, +{ + let (tx, mut rx) = channel(1); + let mut watcher = RecommendedWatcher::new( + move |res| match res { + Ok(e) => futures::executor::block_on(async { + tx.send(e).await.unwrap(); + }), + Err(e) => error!("Error from file_watcher: {e}"), + }, + Config::default(), + )?; + + info!("Watching directory '{}'", dir.as_ref().display()); + + tokio::spawn(async move { + while let Some(ev) = rx.recv().await { + cb(ev) + } + }); + + watcher.watch(dir.as_ref(), RecursiveMode::Recursive)?; + + Ok(watcher) +} diff --git a/src/live_reload.rs b/src/live_reload.rs new file mode 100644 index 0000000..573c55b --- /dev/null +++ b/src/live_reload.rs @@ -0,0 +1,5 @@ +use tower_livereload::LiveReloadLayer; + +pub fn live_reload_layer() -> LiveReloadLayer { + LiveReloadLayer::new() +} diff --git a/src/main.rs b/src/main.rs index 348c07e..6c3afa9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,56 +3,51 @@ use axum::response::IntoResponse; use axum::routing::get; use axum::{http::StatusCode, response::Html, serve, Router}; use minijinja::context; -use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; -use std::{path::PathBuf, str::FromStr, sync::OnceLock}; -use templates::Templates; -use tokio::sync::mpsc::channel; -use tower_http::services::ServeDir; -use tower_livereload::{LiveReloadLayer, Reloader}; +use state::State as AppState; pub use tracing::{debug, error, event, info, span, warn, Level}; +use crate::live_reload::live_reload_layer; + #[derive(Debug)] struct Berr(Box); type Besult = std::result::Result; +mod file_watcher; +mod live_reload; mod observe; +mod state; +mod static_files; mod tailwind; mod templates; -fn static_file_dir() -> &'static PathBuf { - static STATIC_FILE_DIR: OnceLock = OnceLock::new(); - STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) -} - -#[derive(Clone)] -struct AppState { - templates: Templates, -} - #[tokio::main] async fn main() -> Besult<()> { // load configuration? let _setup_logging = observe::setup_logging(); - // TODO: reload templates when they change? separate watcher? - let templates = Templates::try_load().await?; - let mut tt = templates.clone(); - let templates_watcher = tt.start_watcher(); + let state = AppState::try_new().await?; // TODO: only start tailwind if in dev mode? - tokio::spawn(async { tailwind::start_watcher() }); - tokio::spawn(async move { templates_watcher.await }); + tokio::spawn(async move { tailwind::start_watcher() }); - // pulling the watcher into main's scope lets it live until the program dies - let (rl_layer, _watcher) = live_reload_layer()?; + let lr = live_reload_layer(); + + { + // TODO: only start watcher for dev mode? + let state = state.clone(); + let lr = lr.reloader(); + tokio::spawn(async move { state.templates.start_watcher(Some(lr)) }); + } + + let (static_file_service, _static_file_watcher) = static_files::router(Some(lr.reloader()))?; let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - info!("Listening on {listener:#?}"); + + info!("Listening on {listener:?}"); let router = Router::new() .route("/", get(index)) - .nest_service("/static", ServeDir::new(static_file_dir())) - .layer(rl_layer) - .with_state(AppState { templates }); + .nest_service("/static", static_file_service) + .with_state(state); Ok(serve(listener, router).await?) } @@ -66,50 +61,6 @@ async fn index(State(state): State) -> Besult> { )) } -fn live_reload_layer() -> Besult<(LiveReloadLayer, RecommendedWatcher)> { - let rl_layer = LiveReloadLayer::new(); - let rl = rl_layer.reloader(); - let watcher = static_file_watcher(rl)?; - Ok((rl_layer, watcher)) -} - -fn static_file_watcher( - reloader: Reloader, -) -> Result> { - info!("Creating async watcher..."); - - let (tx, mut rx) = channel(1); - - info!("Creating watcher..."); - // watcher needs to move out of scope to live long enough to watch - let mut watcher = RecommendedWatcher::new( - move |res| { - info!("Res from watcher: {res:#?}"); - futures::executor::block_on(async { - tx.send(res).await.unwrap(); - }) - }, - Config::default(), - )?; - info!("Created watcher"); - - watcher.watch(static_file_dir(), RecursiveMode::Recursive)?; - - tokio::spawn(async move { - while let Some(res) = rx.recv().await { - match res { - Ok(event) => { - info!("fs event: {event:#?}"); - reloader.reload() - } - Err(e) => println!("watch error: {:?}", e), - } - } - }); - - Ok(watcher) -} - impl IntoResponse for Berr { fn into_response(self) -> axum::http::Response { error!("webserver error: {:?}", self.0); diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..43b1a63 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,16 @@ +use crate::templates::Templates; +use crate::Besult; +use std::sync::Arc; + +#[derive(Clone)] +pub struct State { + pub templates: Arc, +} + +impl State { + pub async fn try_new() -> Besult { + let templates = Arc::new(Templates::try_load("src/templates").await?); + + Ok(Self { templates }) + } +} diff --git a/src/static_files.rs b/src/static_files.rs new file mode 100644 index 0000000..1abd792 --- /dev/null +++ b/src/static_files.rs @@ -0,0 +1,30 @@ +use crate::file_watcher::prelude::*; + +use crate::Besult; +use axum::Router; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::OnceLock; +use tower_http::services::ServeDir; +use tower_livereload::Reloader; +use tracing::{info, instrument}; + +fn static_file_dir() -> &'static PathBuf { + static STATIC_FILE_DIR: OnceLock = OnceLock::new(); + STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap()) +} + +#[instrument] +pub fn router(reloader: Option) -> Besult<(Router, Option)> { + let watcher = if let Some(rl) = reloader { + Some(file_watcher(static_file_dir(), move |ev| { + info!("Static File Watcher Event: {ev:#?}"); + rl.reload() + })?) + } else { + None + }; + + let router = Router::new().nest_service("/", ServeDir::new(static_file_dir())); + Ok((router, watcher)) +} diff --git a/src/templates.rs b/src/templates.rs index b1b0c03..049303d 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,36 +1,72 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use minijinja::Environment; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use pathdiff::diff_paths; -use tokio::sync::Mutex; -use tracing::info; +use tokio::sync::{mpsc::channel, Mutex}; +use tower_livereload::Reloader; +use tracing::{error, info}; + +use crate::{file_watcher::file_watcher, Besult}; #[derive(Clone)] pub struct Templates { env: Arc>>, + dir: PathBuf, } pub type Error = Box; impl Templates { - pub fn empty() -> Self { + pub fn for_dir>(dir: P) -> Self { let env = Arc::new(Mutex::new(Environment::new())); - Self { env } + Self { + env, + dir: dir.into(), + } } - pub async fn try_load() -> Result { - let mut result = Self::empty(); + pub async fn try_load>(dir: P) -> Result { + let result = Self::for_dir(dir); result.load_env().await?; Ok(result) } - pub async fn start_watcher(&mut self) { - info!("TODO: Implement template watcher"); + pub async fn start_watcher( + self: Arc, + reloader: Option, + ) -> Besult> { + if let Some(rl) = reloader { + Ok(Some(self.watch(rl).await?)) + } else { + Ok(None) + } } - pub async fn load_env(&mut self) -> Result<(), Error> { + async fn watch(self: Arc, reloader: Reloader) -> Besult { + let (tx, mut rx) = channel(1); + let watcher = file_watcher(self.dir.clone(), move |ev| { + info!("Template File Watcher Event: {ev:#?}"); + match tx.blocking_send(ev) { + Ok(_) => {} + Err(err) => error!("Error sending template file watcher event: {err}"), + }; + })?; + let us = self.clone(); + tokio::spawn(async move { + while let Some(_) = rx.recv().await { + us.load_env() + .await + .expect("Failed to reload templates after template changed during runtime"); + reloader.reload() + } + }); + Ok(watcher) + } + + pub async fn load_env(&self) -> Result<(), Error> { info!("Loading templates..."); - for d in walkdir::WalkDir::new("src/templates") { + for d in walkdir::WalkDir::new(&self.dir) { match d { Ok(d) => { if d.file_type().is_dir() { @@ -42,7 +78,6 @@ impl Templates { .into_owned(); info!("Loading template {filename:#?} ({d:#?})"); self.env - .clone() .lock() .await .add_template_owned(filename, std::fs::read_to_string(d.path())?)?;