use std::{net::SocketAddr, path::PathBuf, result::Result as StdResult};
use axum::{response::Html, routing::get, Router};
use btconsole_client::App;
use btlib::{bterr, Result};
use bytes::{Bytes, BytesMut};
use futures::FutureExt;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{sync::oneshot, task::JoinHandle};
use tower::Service;
use tower_http::services::ServeDir;
use yew::ServerRenderer;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct BtConsoleConfig {
addr: SocketAddr,
#[serde(rename = "distpath")]
dist_path: PathBuf,
}
impl BtConsoleConfig {
pub fn new(addr: SocketAddr, asset_path: PathBuf) -> Self {
Self {
addr,
dist_path: asset_path,
}
}
}
impl Default for BtConsoleConfig {
fn default() -> Self {
Self {
addr: SocketAddr::from(([127, 0, 0, 1], 3000)),
dist_path: "../../target/dist".into(),
}
}
}
pub struct BtConsole {
handle: JoinHandle<StdResult<(), hyper::Error>>,
stop: Option<oneshot::Sender<()>>,
}
impl BtConsole {
fn new(handle: JoinHandle<StdResult<(), hyper::Error>>, stop: oneshot::Sender<()>) -> Self {
Self {
handle,
stop: Some(stop),
}
}
pub async fn start(config: BtConsoleConfig) -> Result<Self> {
let rendered: Bytes = {
let index_path = config.dist_path.join("index.html");
let (index, rendered) = tokio::join!(
tokio::fs::read_to_string(&index_path),
ServerRenderer::<App>::new().render(),
);
let index = index?;
let regex = Regex::new(r"</?body>")?;
let fields = regex.split(&index);
let mut bytes = BytesMut::with_capacity(index.len() + rendered.len() + 13);
for (index, entry) in fields.enumerate() {
if 1 == index {
bytes.extend_from_slice(b"<body>");
bytes.extend_from_slice(rendered.as_bytes());
bytes.extend_from_slice(b"</body>");
} else {
bytes.extend_from_slice(entry.as_bytes());
}
}
bytes.into()
};
let mut serve_dir = ServeDir::new(config.dist_path).append_index_html_on_directories(false);
let app = Router::new()
.route("/", get(|| async move { Html(rendered) }))
.route("/*path", get(move |req| serve_dir.call(req)));
log::info!("listening at http://{}", config.addr);
let (tx, rx) = oneshot::channel::<()>();
let server = axum::Server::bind(&config.addr)
.serve(app.into_make_service())
.with_graceful_shutdown(rx.map(|result| result.unwrap()));
let handle = tokio::spawn(server);
Ok(Self::new(handle, tx))
}
pub fn signal_shutdown(&mut self) -> Result<()> {
if let Some(stop) = self.stop.take() {
stop.send(())
.map_err(|_| bterr!("failed to send stop signal to server"))?;
}
Ok(())
}
pub fn take_shutdown_signal(&mut self) -> Option<oneshot::Sender<()>> {
self.stop.take()
}
pub async fn has_shutdown(self) -> Result<()> {
self.handle.await?.map_err(|err| err.into())
}
}