#![feature(future_join)] use std::{env, path::PathBuf}; use actix_files; use actix_web::{ dev::ServiceResponse, error, http::{header::ContentType, StatusCode}, middleware::{ErrorHandlerResponse, ErrorHandlers, Logger}, web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, Result, }; use actix_web_lab::respond::Html; use async_channel; use futures::{pin_mut, select, FutureExt}; use minijinja; use minijinja_autoreload::AutoReloader; use crate::util::CustomResult; use crate::webui::worker::worker_loop; mod views; mod worker; struct MiniJinjaRenderer { tmpl_env: web::Data, } impl MiniJinjaRenderer { fn render(&self, tmpl: &str, ctx: impl Into) -> Result { log::info!("Rendering template {:?}", tmpl); self.tmpl_env .acquire_env() .map_err(|_| error::ErrorInternalServerError("could not acquire template env"))? .get_template(tmpl) .map_err(|e| error::ErrorInternalServerError(format!("{:?}", e)))? .render(ctx.into()) .map(Html) .map_err(|err| { log::error!("{err}"); error::ErrorInternalServerError("template error") }) } } impl FromRequest for MiniJinjaRenderer { type Error = actix_web::Error; type Future = actix_utils::future::Ready>; fn from_request(req: &HttpRequest, _pl: &mut actix_web::dev::Payload) -> Self::Future { let tmpl_env = >::extract(req) .into_inner() .unwrap(); actix_utils::future::ready(Ok(Self { tmpl_env })) } } #[tokio::main] pub async fn web_ui_main(port: u16, enable_template_autoreload: bool) -> CustomResult<()> { // The closure is invoked every time the environment is outdated to recreate it. let tmpl_reloader = AutoReloader::new(move |notifier| { let mut env: minijinja::Environment<'static> = minijinja::Environment::new(); let tmpl_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/templates"); log::info!("Using template dir {:?}", tmpl_path); // if watch_path is never called, no fs watcher is created if enable_template_autoreload { notifier.watch_path(&tmpl_path, true); } env.set_source(minijinja::Source::from_path(tmpl_path)); Ok(env) }); let (s, r) = async_channel::unbounded::(); let tmpl_reloader = web::Data::new(tmpl_reloader); let server = HttpServer::new(move || { App::new() .app_data(tmpl_reloader.clone()) .app_data(web::Data::new(s.clone())) .service( actix_files::Files::new("/static", "./resources/static/").use_last_modified(true), ) .service(views::index) .service(views::actions) .service(views::overview) .service(views::overview_submit_button) .service(views::actions_partials) .service(views::push_event) .wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)) .wrap(Logger::default()) }) .workers(2) .bind(("0.0.0.0", port)) .unwrap(); let t1 = server.run().fuse(); let t2 = worker_loop(r).fuse(); pin_mut!(t1, t2); select! { x = t1 => x, y = t2 => y, }; Ok(()) } /// Error handler for a 404 Page not found error. fn not_found(svc_res: ServiceResponse) -> Result> { let res = get_error_response(&svc_res, "Page not found"); Ok(ErrorHandlerResponse::Response(ServiceResponse::new( svc_res.into_parts().0, res.map_into_right_body(), ))) } /// Generic error handler. fn get_error_response(res: &ServiceResponse, error: &str) -> HttpResponse { let req = res.request(); let tmpl_env = MiniJinjaRenderer::extract(req).into_inner().unwrap(); // Provide a fallback to a simple plain text response in case an error occurs during the // rendering of the error page. let fallback = |err: &str| { HttpResponse::build(res.status()) .content_type(ContentType::plaintext()) .body(err.to_string()) }; let ctx = minijinja::context! { error => error, status_code => res.status().as_str(), }; match tmpl_env.render("error.html", ctx) { Ok(body) => body .customize() .with_status(res.status()) .respond_to(req) .map_into_boxed_body(), Err(_) => fallback(error), } }