slave-framework 0.6.0

Runtime for slave apps (a Laravel-inspired Rust framework for MCP-first apps): server lifecycle, config, logging, Turso Cloud database and migrations, and OpenID Connect sign-in
Documentation
//! HTTP server lifecycle.

use axum::Router;
use tokio::net::TcpListener;

use crate::{Error, Result};

/// Binds to `host:port` and serves `router` until Ctrl+C or SIGTERM is received.
///
/// In-flight requests are allowed to finish before this function returns.
pub async fn serve(router: Router, host: &str, port: u16) -> Result<()> {
    let listener = TcpListener::bind((host, port))
        .await
        .map_err(|source| Error::Bind {
            addr: format!("{host}:{port}"),
            source,
        })?;
    let addr = listener.local_addr().map_err(Error::Server)?;
    tracing::info!("Server running on http://{addr}");

    axum::serve(listener, router)
        .with_graceful_shutdown(shutdown_signal())
        .await
        .map_err(Error::Server)?;

    tracing::info!("Server stopped");
    Ok(())
}

/// Resolves when the process receives Ctrl+C, or SIGTERM on Unix.
pub async fn shutdown_signal() {
    let ctrl_c = async {
        if tokio::signal::ctrl_c().await.is_err() {
            std::future::pending::<()>().await;
        }
    };

    #[cfg(unix)]
    let terminate = async {
        use tokio::signal::unix::{SignalKind, signal};
        match signal(SignalKind::terminate()) {
            Ok(mut sigterm) => {
                sigterm.recv().await;
            }
            Err(_) => std::future::pending::<()>().await,
        }
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        () = ctrl_c => {},
        () = terminate => {},
    }
    tracing::info!("Shutting down gracefully...");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn reports_bind_errors_with_address() {
        let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let port = taken.local_addr().unwrap().port();

        let err = serve(Router::new(), "127.0.0.1", port).await.unwrap_err();
        assert!(matches!(err, Error::Bind { .. }), "{err}");
        assert!(err.to_string().contains(&port.to_string()));
    }
}