mod cors;
mod handlers;
pub mod server_info;
mod spa;
use std::{
fmt::Debug,
future::{Future, IntoFuture},
net::{IpAddr, Ipv4Addr, SocketAddr},
pin::Pin,
sync::Arc,
time::Duration,
};
use axum::{routing::any, Router};
use local_ip_address::local_ip;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::{json, Value};
use tokio::{
net::TcpListener,
sync::{oneshot, RwLock},
task::JoinHandle,
};
use tracing::{error, info};
pub use server_info::{ServerInfo, ServerStatus};
use crate::{
router::{message::Message, request::split_resource_and_operation, NoSocketRouter, RestRouter, SocketRouter},
socket::{
connection::Connections,
fanout::{Fanout, LocalOnly},
SocketConfig,
},
};
use self::handlers::AppState;
pub type PageMetaFn = Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = Option<String>> + Send>> + Send + Sync>;
#[derive(Clone)]
pub struct PageMeta(pub PageMetaFn);
impl Debug for PageMeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PageMeta(..)")
}
}
pub struct ServerConfig<R> {
pub id: String,
pub port: String,
pub path: Option<String>,
pub client_dir: Option<String>,
pub router: R,
pub heartbeat: Option<Duration>,
pub allowed_origins: Option<Vec<String>>,
pub page_meta: Option<PageMeta>,
pub max_body: Option<usize>,
pub public_ip: Option<String>,
pub fanout: Option<Arc<dyn Fanout>>,
pub bind_addr: IpAddr,
}
impl<R> Debug for ServerConfig<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerConfig")
.field("id", &self.id)
.field("port", &self.port)
.field("path", &self.path)
.field("client_dir", &self.client_dir)
.field("heartbeat", &self.heartbeat)
.field("allowed_origins", &self.allowed_origins)
.field("page_meta", &self.page_meta)
.field("max_body", &self.max_body)
.field("bind_addr", &self.bind_addr)
.finish_non_exhaustive()
}
}
impl<R> ServerConfig<R> {
pub fn new(
id: &str,
port: &str,
client_dir: Option<String>,
router: R,
heartbeat: Option<u64>,
allowed_origins: Option<Vec<String>>,
path: Option<String>,
) -> Self {
Self {
id: id.into(),
port: port.into(),
path,
client_dir,
router,
heartbeat: heartbeat.map(Duration::from_secs),
allowed_origins,
page_meta: None,
max_body: None,
public_ip: None,
fanout: None,
bind_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
}
}
pub fn with_page_meta(mut self, page_meta: PageMetaFn) -> Self {
self.page_meta = Some(PageMeta(page_meta));
self
}
pub fn with_max_body(mut self, bytes: usize) -> Self {
self.max_body = Some(bytes);
self
}
pub fn with_public_ip(mut self, ip: &str) -> Self {
self.public_ip = Some(ip.to_string());
self
}
pub fn with_fanout(mut self, fanout: Arc<dyn Fanout>) -> Self {
self.fanout = Some(fanout);
self
}
pub fn with_bind_addr(mut self, addr: IpAddr) -> Self {
self.bind_addr = addr;
self
}
}
pub struct RunningServer {
addr: SocketAddr,
connections: Connections,
fanout: Arc<dyn Fanout>,
server_info: Arc<RwLock<ServerInfo>>,
shutdown: Option<oneshot::Sender<()>>,
serve_task: JoinHandle<std::io::Result<()>>,
heartbeat_task: Option<JoinHandle<()>>,
}
impl RunningServer {
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn port(&self) -> u16 {
self.addr.port()
}
pub fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.addr.port())
}
pub fn connections(&self) -> &Connections {
&self.connections
}
pub fn server_info(&self) -> Arc<RwLock<ServerInfo>> {
self.server_info.clone()
}
pub async fn shutdown(mut self) -> anyhow::Result<()> {
if let Some(h) = self.heartbeat_task.take() {
h.abort();
}
self.fanout.shutdown();
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
(&mut self.serve_task).await??;
Ok(())
}
pub async fn wait_for_signal(self) -> anyhow::Result<()> {
shutdown_signal().await;
info!("shutdown signal received");
self.shutdown().await
}
}
impl Drop for RunningServer {
fn drop(&mut self) {
if let Some(h) = self.heartbeat_task.take() {
h.abort();
}
self.fanout.shutdown();
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
}
}
async fn shutdown_signal() {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let terminate = async {
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
Ok(mut s) => {
s.recv().await;
}
Err(_) => std::future::pending::<()>().await,
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
pub struct CnctdServer;
impl CnctdServer {
pub async fn bind<R, SReq, SResp, SR>(
config: ServerConfig<R>,
socket_config: Option<SocketConfig<SR>>,
) -> anyhow::Result<RunningServer>
where
R: RestRouter,
SReq: DeserializeOwned + Send + 'static,
SResp: Serialize + Send + 'static,
SR: SocketRouter<SReq, SResp>,
{
let prefix = config.path.clone().unwrap_or_else(|| "api".to_string());
let port: u16 = config.port.parse()?;
let fanout: Arc<dyn Fanout> = match config.fanout.clone() {
Some(f) => f,
None => match socket_config.as_ref().and_then(|s| s.redis_url.as_deref()) {
#[cfg(feature = "redis")]
Some(url) => Arc::new(crate::socket::redis_fanout::RedisFanout::connect(url, &config.id).await?),
#[cfg(not(feature = "redis"))]
Some(_) => {
tracing::warn!("SocketConfig.redis_url set but the `redis` feature is off; running single-instance");
Arc::new(LocalOnly)
}
None => Arc::new(LocalOnly),
},
};
let connections = Connections::new(&config.id, fanout.clone());
connections.install_global();
fanout.attach(connections.clone()).await?;
let listener = TcpListener::bind(SocketAddr::new(config.bind_addr, port)).await?;
let addr = listener.local_addr()?;
let local = local_ip().map(|ip| ip.to_string()).unwrap_or_else(|_| "127.0.0.1".to_string());
let server_info = Arc::new(RwLock::new(server_info::new_server_info(
connections.identity(),
&local,
config.public_ip.clone(),
addr.port(),
socket_config.is_some(),
socket_config.as_ref().and_then(|s| s.redis_url.clone()),
)));
let state = AppState {
router: Arc::new(config.router),
prefix: prefix.clone(),
max_body: config.max_body,
};
let mut app: Router = Router::new()
.route(&format!("/{}", prefix), any(handlers::api::<R>))
.route(&format!("/{}/{{*rest}}", prefix), any(handlers::api::<R>))
.route("/redirect", any(handlers::api::<R>))
.route("/redirect/{*rest}", any(handlers::api::<R>))
.with_state(state);
let has_socket = socket_config.is_some();
if let Some(sc) = socket_config {
app = app.merge(crate::socket::router::<SReq, SResp, SR>(sc, connections.clone()));
}
app = match &config.client_dir {
Some(dir) => app.fallback_service(spa::router(dir, config.page_meta.clone().map(|m| m.0))),
None => app.fallback(handlers::not_found),
};
let app = app.layer(cors::layer(config.allowed_origins.clone()));
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let serve_task = tokio::spawn(
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(async {
let _ = shutdown_rx.await;
})
.into_future(),
);
let heartbeat_task = config.heartbeat.map(|every| {
let connections = connections.clone();
let server_info = server_info.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(every).await;
let count = connections.count().await;
let snapshot = {
let mut info = server_info.write().await;
server_info::refresh(&mut info, count);
info.clone()
};
let msg = Message::new("server-info", "heartbeat", Some(json!(snapshot)));
if let Err(e) = connections.broadcast(&msg).await {
error!("heartbeat broadcast failed: {}", e);
}
}
})
});
info!(
id = %config.id,
"server{} running at http://{}:{}",
if has_socket { " and socket" } else { "" },
local,
addr.port()
);
Ok(RunningServer {
addr,
connections,
fanout,
server_info,
shutdown: Some(shutdown_tx),
serve_task,
heartbeat_task,
})
}
pub async fn start<R, SReq, SResp, SR>(
server_config: ServerConfig<R>,
socket_config: Option<SocketConfig<SR>>,
) -> anyhow::Result<()>
where
R: RestRouter,
SReq: DeserializeOwned + Send + 'static,
SResp: Serialize + Send + 'static,
SR: SocketRouter<SReq, SResp>,
{
Self::bind(server_config, socket_config).await?.wait_for_signal().await
}
pub async fn start_with_shutdown<R, SReq, SResp, SR, F>(
server_config: ServerConfig<R>,
socket_config: Option<SocketConfig<SR>>,
signal: F,
) -> anyhow::Result<()>
where
R: RestRouter,
SReq: DeserializeOwned + Send + 'static,
SResp: Serialize + Send + 'static,
SR: SocketRouter<SReq, SResp>,
F: Future<Output = ()> + Send,
{
let running = Self::bind(server_config, socket_config).await?;
signal.await;
running.shutdown().await
}
pub async fn start_without_socket<R: RestRouter>(server_config: ServerConfig<R>) -> anyhow::Result<()> {
Self::start::<R, Value, Value, NoSocketRouter>(server_config, None).await
}
pub fn path_to_resource_and_operation(path: &str) -> (String, Option<String>) {
split_resource_and_operation(path, "api")
}
}