use crate::interpreted::load_new_template_system;
use arc_swap::ArcSwap;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Extension, Router};
use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem;
use hornbeam_interpreter::LoadedTemplates;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::error::Error;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::mpsc::RecvTimeoutError;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::net::TcpListener;
use tokio::sync::oneshot;
pub(crate) fn start_hot_reloader(
default_locale: String,
templates: Arc<ArcSwap<LoadedTemplates<FluentLocalisationSystem>>>,
template_dir: PathBuf,
translation_dir: PathBuf,
) {
let auto_reloader_waiting_queue = start_auto_hot_reloader();
std::thread::Builder::new()
.name(String::from("Hornbeam Hot Reload"))
.spawn(move || {
let (notif_tx, notif_rx) = std::sync::mpsc::sync_channel(1);
let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
match res {
Ok(event) => {
let ekind: &EventKind = &event.kind;
match ekind {
EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) => {
notif_tx
.send(())
.expect("Hornbeam: failed to handle file change");
}
EventKind::Access(_) => {}
EventKind::Remove(_) => {}
EventKind::Other => {}
}
}
Err(e) => println!("Hornbeam watch error: {:?}", e),
}
})
.expect("Failed to start Hornbeam watcher");
watcher
.watch(&template_dir, RecursiveMode::Recursive)
.expect("Failed to watch Hornbeam template dir");
watcher
.watch(&translation_dir, RecursiveMode::Recursive)
.expect("Failed to watch Hornbeam translation dir");
let default_locale = default_locale;
loop {
notif_rx.recv().unwrap();
let deadline = Instant::now() + Duration::from_millis(100);
loop {
let now = Instant::now();
if now >= deadline {
break;
}
if let Err(RecvTimeoutError::Disconnected) =
notif_rx.recv_timeout(deadline - now)
{
panic!("Hornbeam watcher: disconnected");
}
}
eprint!("reloading Hornbeam templates... ");
match load_new_template_system(&default_locale, &template_dir, &translation_dir) {
Ok(new_template_system) => {
templates.swap(Arc::new(new_template_system));
eprintln!("fine.");
for waiter in auto_reloader_waiting_queue.lock().unwrap().drain(..) {
let _ = waiter.send(());
}
}
Err(e) => {
eprintln!("failed to hot reload Hornbeam: {e}");
}
}
}
})
.expect("can't start Hornbeam Hot Reload thread");
}
type WaiterList = Arc<Mutex<Vec<oneshot::Sender<()>>>>;
pub(crate) fn start_auto_hot_reloader() -> WaiterList {
let waiter_list = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/wait_hot_reload", get(hot_reload_longpoll))
.layer(Extension(waiter_list.clone()));
let addr = SocketAddr::from(([127, 0, 0, 1], 7015));
tokio::spawn(async move {
let result: Result<(), Box<dyn Error>> = async move {
let listener = TcpListener::bind(addr).await?;
eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
axum::serve(listener, app).await?;
Ok(())
}
.await;
if let Err(e) = result {
eprintln!("Hornbeam Auto Hot Reload failed: {e}");
}
});
waiter_list
}
async fn hot_reload_longpoll(Extension(waiter_list): Extension<WaiterList>) -> impl IntoResponse {
let (tx, rx) = oneshot::channel::<()>();
{
waiter_list.lock().unwrap().push(tx);
}
let _ = rx.await;
"reload"
}