Skip to main content

lightshuttle_control/
server.rs

1//! HTTP server for the local control plane.
2
3use std::future::Future;
4use std::net::SocketAddr;
5
6use lightshuttle_runtime::LifecycleHandle;
7use tokio::net::TcpListener;
8
9use crate::routes::router;
10use crate::state::ControlState;
11
12/// Bind a TCP listener on `addr`. Pass a port of `0` to let the OS pick
13/// a free port; read it back from [`TcpListener::local_addr`].
14///
15/// Exposed as a free function (rather than an associated function on
16/// the generic [`ControlServer`]) so callers do not need a turbofish
17/// to pin the handle type before they own a state value.
18pub async fn bind(addr: SocketAddr) -> std::io::Result<TcpListener> {
19    TcpListener::bind(addr).await
20}
21
22/// HTTP server hosting the control plane.
23pub struct ControlServer<H>
24where
25    H: LifecycleHandle + Clone + Send + Sync + 'static,
26{
27    state: ControlState<H>,
28}
29
30impl<H> ControlServer<H>
31where
32    H: LifecycleHandle + Clone + Send + Sync + 'static,
33{
34    /// Build a server bound to `state`.
35    #[must_use]
36    pub fn new(state: ControlState<H>) -> Self {
37        Self { state }
38    }
39
40    /// Consume the server and return the underlying axum router. Useful
41    /// for in-process integration tests via `tower::ServiceExt::oneshot`,
42    /// which avoid the cost of a real TCP bind.
43    pub fn into_router(self) -> axum::Router {
44        router(self.state)
45    }
46
47    /// Serve the control plane on `listener` until `shutdown` resolves.
48    ///
49    /// Performs a graceful shutdown that drains in-flight requests
50    /// before returning.
51    pub async fn serve<F>(self, listener: TcpListener, shutdown: F) -> std::io::Result<()>
52    where
53        F: Future<Output = ()> + Send + 'static,
54    {
55        let app = router(self.state);
56        axum::serve(listener, app)
57            .with_graceful_shutdown(shutdown)
58            .await
59    }
60}