use crate::config::AppConfig;
use crate::middleware;
use crate::router::HttpRouter;
use polaris_system::api::API;
use polaris_system::plugin::{Plugin, PluginId, Version};
use polaris_system::server::Server;
use tokio::sync::watch;
pub struct ServerHandle {
shutdown_tx: parking_lot::Mutex<Option<watch::Sender<bool>>>,
handle: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl std::fmt::Debug for ServerHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerHandle")
.field("running", &self.handle.lock().is_some())
.finish()
}
}
impl API for ServerHandle {}
impl ServerHandle {
pub fn shutdown(&self) -> bool {
if let Some(tx) = self.shutdown_tx.lock().take() {
let _ = tx.send(true);
true
} else {
false
}
}
}
pub struct AppPlugin {
config: AppConfig,
listener: parking_lot::Mutex<Option<tokio::net::TcpListener>>,
}
impl std::fmt::Debug for AppPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AppPlugin")
.field("config", &self.config)
.field("has_listener", &self.listener.lock().is_some())
.finish()
}
}
impl AppPlugin {
#[must_use]
pub fn new(config: AppConfig) -> Self {
Self {
config,
listener: parking_lot::Mutex::new(None),
}
}
#[must_use]
pub fn with_listener(self, listener: tokio::net::TcpListener) -> Self {
*self.listener.lock() = Some(listener);
self
}
}
impl Plugin for AppPlugin {
const ID: &'static str = "polaris::app";
const VERSION: Version = Version::new(0, 0, 1);
fn build(&self, server: &mut Server) {
server.insert_global(self.config.clone());
server.insert_api(HttpRouter::new());
}
async fn ready(&self, server: &mut Server) {
let router_api = server
.api::<HttpRouter>()
.expect("HttpRouter API must exist (registered in build)");
let fragments = router_api.take_routes();
let auth = router_api.take_auth();
let mut app = axum::Router::new();
for fragment in fragments {
app = app.merge(fragment);
}
let (app, addr) = {
let config = server
.get_global::<AppConfig>()
.expect("AppConfig must exist (registered in build)");
let app = middleware::apply_middleware(app, &config, auth);
let addr = config.addr();
(app, addr)
};
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let injected_listener = self.listener.lock().take();
let addr_for_log = addr;
let handle = tokio::spawn(async move {
let listener = if let Some(listener) = injected_listener {
tracing::info!(
addr = %listener.local_addr().expect("listener must have local addr"),
"HTTP server listening (pre-bound)"
);
listener
} else {
match tokio::net::TcpListener::bind(addr).await {
Ok(listener) => {
tracing::info!(addr = %addr, "HTTP server listening");
listener
}
Err(bind_err) => {
tracing::error!(
addr = %addr,
error = %bind_err,
"failed to bind HTTP server — no routes will be served. \
Check that the port is not already in use."
);
return;
}
}
};
let shutdown_signal = create_shutdown_signal(shutdown_rx);
if let Err(serve_err) = axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal)
.await
{
tracing::error!(
addr = %addr_for_log,
error = %serve_err,
"HTTP server error"
);
}
});
server.insert_api(ServerHandle {
shutdown_tx: parking_lot::Mutex::new(Some(shutdown_tx)),
handle: parking_lot::Mutex::new(Some(handle)),
});
}
async fn cleanup(&self, server: &mut Server) {
let Some(server_handle) = server.api::<ServerHandle>() else {
return;
};
server_handle.shutdown();
let handle = server_handle.handle.lock().take();
if let Some(handle) = handle {
let timeout = std::time::Duration::from_secs(5);
match tokio::time::timeout(timeout, handle).await {
Ok(Ok(())) => tracing::info!("HTTP server shut down gracefully"),
Ok(Err(join_err)) => {
tracing::warn!(error = %join_err, "HTTP server task panicked");
}
Err(_elapsed) => {
tracing::warn!(
timeout_secs = timeout.as_secs(),
"HTTP server shutdown timed out"
);
}
}
}
}
fn dependencies(&self) -> Vec<PluginId> {
Vec::new()
}
}
async fn create_shutdown_signal(mut rx: watch::Receiver<bool>) {
let _ = rx.wait_for(|&val| val).await;
tracing::debug!("HTTP server shutdown signal received");
}