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}