Something went wrong. Try again.
An opinionated framework for small task-based applications.
Something went wrong. Try again.
6.6 kB · 215 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216//! The web interface for wallsocket//! See the main crate for more.// TODO: more here#![allow(clippy::result_large_err)]
use minijinja::Environment;use std::{fmt::Debug, fs::File, io::Read, ops::Deref, sync::Arc};use thiserror::Error;use wallsocket_core::{Database, LogRetrievalError, LogRetriever};
#[cfg(debug_assertions)]use std::{path::PathBuf, str::FromStr};#[cfg(debug_assertions)]use tower_http::services::ServeFile;
#[cfg(not(debug_assertions))]use axum::{body::Body, response::Response};#[cfg(not(debug_assertions))]use include_dir::{Dir, include_dir};#[cfg(not(debug_assertions))]use tower::service_fn;
use include_dir as _;
use axum::Router;use tokio::net::ToSocketAddrs;use tracing::info;
mod helpers;mod html;
/// Configuration for the web interface.pub struct Config<D> { /// User-recognisable name to show. pub service_name: String,
/// Where to retrieve logs from. pub log_retriever: LogRetriever<D>,
/// Where to look for pending input requests. #[cfg(feature = "input")] pub input_registry: Arc<wallsocket_input::InputRegistry>,}
/// State shared with the whole axum application.#[derive(Debug)]pub(crate) struct AppState<D> { /// The config we were started with. pub config: Config<D>,
/// minijinja templating environment. pub templates: Environment<'static>,}
/// Serve the web interface with the given address and config./// You will often want to background this process, ie using [`tokio::spawn`]./// # Errors/// If fatal errors are raised by axum./// If we can't bind to the requested port./// # Errors/// Errors if it can't get the list of available log sinks.pub async fn serve<D: Deref<Target = Database> + Send + Sync + 'static>( addr: impl ToSocketAddrs + Debug, config: Config<D>,) -> Result<(), ServeError> { let router = router(config)?;
info!("starting to listen on {:?}", addr); axum::serve(tokio::net::TcpListener::bind(addr).await?, router).await?;
Ok(())}
/// An error encountered when serving the web interface.#[derive(Debug, Error)]pub enum ServeError { /// IO Error. Most likely failing to bind to the port. #[error("io error: {0}")] Io(#[from] std::io::Error),
/// An error getting sinks from the database. #[error("error getting available sinks: {0}")] LogRetrieval(#[from] LogRetrievalError),}
/// Build an axum [`Router`] that runs the web interface./// This is useful when integrating with a larger axum app./// # Errors/// Errors if it can't get the list of available log sinks.pub fn router<D: Deref<Target = Database> + Send + Sync + 'static>( config: Config<D>,) -> Result<Router, LogRetrievalError> { info!( message = "building router for wallsocket_web", service_name = config.service_name, wallsocket_web_version = env!("CARGO_PKG_VERSION") );
let mut router = Router::new(); let mut templates = Environment::new();
#[cfg(debug_assertions)] let (static_path, templates_path) = { info!("in debug mode, serving static files from filesystem"); let mut path: PathBuf = file!().into(); path.pop(); (path.join("../static"), path.join("../templates")) };
#[cfg(debug_assertions)] macro_rules! serve_file { ($path:expr, $file:expr, $mime:expr) => { router = router.route_service( $path, ServeFile::new_with_mime( static_path.join($file), &FromStr::from_str($mime).unwrap(), ), ) }; }
#[cfg(debug_assertions)] macro_rules! add_template { ($name:expr, $file:expr) => { templates .add_template_owned($name, { let mut buf = String::new(); File::open(templates_path.join($file)) .unwrap() .read_to_string(&mut buf) .unwrap();
buf }) .unwrap(); }; }
#[cfg(not(debug_assertions))] static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static");
#[cfg(not(debug_assertions))] static TEMPLATES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
#[cfg(not(debug_assertions))] macro_rules! serve_file { ($path:expr, $file:expr, $mime:expr) => {{ let f = STATIC_DIR.get_file($file).unwrap(); router = router.route_service( $path, service_fn(async |_| { Ok({ Response::builder() .header("Content-Type", $mime) .body(Into::<Body>::into(f.contents().to_vec())) .unwrap() }) }), ) }}; }
#[cfg(not(debug_assertions))] macro_rules! add_template { ($name:expr, $file:expr) => { let f = TEMPLATES_DIR.get_file($file).unwrap(); let contents = f.contents_utf8().unwrap();
templates.add_template_owned($name, contents).unwrap(); }; }
serve_file!("/css.css", "css.css", "text/css"); serve_file!("/js.js", "js.js", "text/javascript");
add_template!("base", "base.html.j2"); add_template!("index", "index.html.j2"); add_template!("pending_inputs", "pending_inputs.html.j2"); add_template!("log_previews", "log_previews.html.j2"); add_template!("log_data", "log_data.html.j2");
templates.add_global("service_name", config.service_name.clone()); templates.add_global("version", env!("CARGO_PKG_VERSION"));
let available_sinks = config.log_retriever.available_sinks()?; info!( message = "got available sinks", count = available_sinks.len() ); templates.add_global("available_sinks", available_sinks);
{ #[cfg(feature = "input")] let with_input = true;
#[cfg(not(feature = "input"))] let with_input = false;
info!(with_input_module = with_input); templates.add_global("with_input", with_input); }
let state = Arc::new(AppState { config, templates });
Ok(router.merge(html::router()).with_state::<()>(state))}
impl<D: Debug> Debug for Config<D> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Config") .field("service_name", &self.service_name) .finish_non_exhaustive() }}