1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! HTTP server for the local control plane.
//!
//! The two public entry points are:
//!
//! - [`bind`]: opens a [`tokio::net::TcpListener`] before the server is
//! constructed (allows reading back the OS-assigned port when port `0` is
//! passed).
//! - [`ControlServer`]: wraps a [`crate::ControlState`] and drives the axum
//! router until a caller-supplied shutdown future resolves.
use Future;
use SocketAddr;
use LifecycleHandle;
use TcpListener;
use craterouter;
use crateControlState;
/// Open a TCP listener that will be passed to [`ControlServer::serve`].
///
/// Pass a port of `0` to let the OS assign a free port; read it back
/// with [`TcpListener::local_addr`] after the call returns.
///
/// This is a free function rather than an associated function on the
/// generic [`ControlServer`] so callers can open the socket before
/// constructing state, without needing a turbofish to pin `H`.
///
/// Always bind to a loopback address (`127.0.0.1`) in practice: the
/// control plane carries no authentication and is intended only for
/// local developer use.
///
/// # Errors
///
/// Returns an [`std::io::Error`] if the bind fails (address already in
/// use, permission denied, etc.).
///
/// # Example
///
/// ```rust,no_run
/// use std::net::SocketAddr;
/// use lightshuttle_control::bind;
///
/// # async fn run() -> std::io::Result<()> {
/// // Loopback only: the control plane has no authentication.
/// let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
/// let listener = bind(addr).await?;
/// let port = listener.local_addr()?.port();
/// println!("control plane listening on port {port}");
/// # Ok(())
/// # }
/// ```
pub async
/// HTTP server that hosts the control plane router.
///
/// Generic over `H`, which must implement
/// [`lightshuttle_runtime::LifecycleHandle`]. The handle is held inside a
/// [`crate::ControlState`] and shared across all route handlers via axum's
/// state mechanism.
///
/// # Usage
///
/// 1. Call [`bind`] to open a listener (loopback only, no authentication).
/// 2. Build a [`crate::ControlState`] with the project name and handle.
/// 3. Construct a [`ControlServer`] via [`ControlServer::new`].
/// 4. Await [`ControlServer::serve`] with a shutdown future.
///
/// For in-process integration tests, use [`ControlServer::into_router`] to
/// get the raw axum router and drive it with `tower::ServiceExt::oneshot`
/// without opening a TCP socket.