Progress on modularizing watchers

This commit is contained in:
Daniel Flanagan 2024-05-14 12:28:27 -05:00
parent 1843f9fac3
commit 2052092796
6 changed files with 163 additions and 84 deletions

42
src/file_watcher.rs Normal file
View file

@ -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<P, F>(dir: P, cb: F) -> Besult<FileWatcher>
where
P: AsRef<Path>,
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)
}

5
src/live_reload.rs Normal file
View file

@ -0,0 +1,5 @@
use tower_livereload::LiveReloadLayer;
pub fn live_reload_layer() -> LiveReloadLayer {
LiveReloadLayer::new()
}

View file

@ -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<dyn std::error::Error>);
type Besult<T> = std::result::Result<T, Berr>;
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<PathBuf> = 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<AppState>) -> Besult<Html<String>> {
))
}
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<RecommendedWatcher, Box<dyn std::error::Error>> {
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<axum::body::Body> {
error!("webserver error: {:?}", self.0);

16
src/state.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::templates::Templates;
use crate::Besult;
use std::sync::Arc;
#[derive(Clone)]
pub struct State {
pub templates: Arc<Templates>,
}
impl State {
pub async fn try_new() -> Besult<Self> {
let templates = Arc::new(Templates::try_load("src/templates").await?);
Ok(Self { templates })
}
}

30
src/static_files.rs Normal file
View file

@ -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<PathBuf> = OnceLock::new();
STATIC_FILE_DIR.get_or_init(|| PathBuf::from_str("static").unwrap())
}
#[instrument]
pub fn router(reloader: Option<Reloader>) -> Besult<(Router, Option<FileWatcher>)> {
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))
}

View file

@ -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<Mutex<Environment<'static>>>,
dir: PathBuf,
}
pub type Error = Box<dyn std::error::Error>;
impl Templates {
pub fn empty() -> Self {
pub fn for_dir<P: Into<PathBuf>>(dir: P) -> Self {
let env = Arc::new(Mutex::new(Environment::new()));
Self { env }
Self {
env,
dir: dir.into(),
}
}
pub async fn try_load() -> Result<Self, Error> {
let mut result = Self::empty();
pub async fn try_load<P: Into<PathBuf>>(dir: P) -> Result<Self, Error> {
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<Self>,
reloader: Option<Reloader>,
) -> Besult<Option<RecommendedWatcher>> {
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<Self>, reloader: Reloader) -> Besult<RecommendedWatcher> {
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())?)?;