Progress on modularizing watchers
This commit is contained in:
parent
1843f9fac3
commit
2052092796
42
src/file_watcher.rs
Normal file
42
src/file_watcher.rs
Normal 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
5
src/live_reload.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use tower_livereload::LiveReloadLayer;
|
||||
|
||||
pub fn live_reload_layer() -> LiveReloadLayer {
|
||||
LiveReloadLayer::new()
|
||||
}
|
95
src/main.rs
95
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<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
16
src/state.rs
Normal 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
30
src/static_files.rs
Normal 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))
|
||||
}
|
|
@ -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())?)?;
|
||||
|
|
Loading…
Reference in a new issue