cratefield_runtime_native/
server.rs1use std::net::SocketAddr;
7use std::sync::Arc;
8
9use axum::extract::{ConnectInfo, Request, State};
10use axum::middleware::{Next, from_fn_with_state};
11use axum::response::Response;
12use cratefield_core::{Config, Harness};
13use thiserror::Error;
14use tokio::net::TcpListener;
15
16use crate::config::EnvConfig;
17use crate::cron::{CronError, spawn_cron_scheduler};
18use crate::ports::{client_ip, normalize_headers, trusted_proxy_headers};
19use crate::runtime::{Native, clone_ports};
20use crate::tracing_setup::install_tracing;
21
22pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080";
27
28#[derive(Debug, Error)]
29pub enum ServeError {
30 #[error("LISTEN_ADDR {addr:?} is not a valid socket address")]
32 ListenAddr { addr: String },
33 #[error("failed to bind {addr}: {source}")]
35 Bind {
36 addr: String,
37 #[source]
38 source: std::io::Error,
39 },
40 #[error("invalid CRONS entry")]
45 Cron(#[from] CronError),
46 #[error("server failed: {0}")]
48 Serve(#[source] std::io::Error),
49}
50
51pub async fn serve(harness: Arc<Harness>, runtime: Native) -> Result<(), ServeError> {
68 install_tracing();
69 let config = Arc::new(EnvConfig);
70 let addr = listen_addr(&*config)?;
71 let listener = TcpListener::bind(addr)
72 .await
73 .map_err(|source| ServeError::Bind {
74 addr: addr.to_string(),
75 source,
76 })?;
77 serve_on(harness, runtime, listener).await
78}
79
80pub async fn serve_on(
91 harness: Arc<Harness>,
92 runtime: Native,
93 listener: TcpListener,
94) -> Result<(), ServeError> {
95 install_tracing();
96
97 let config = Arc::new(EnvConfig);
98 let ports = runtime.ports();
99
100 let crons = cron_expressions(&*config);
104 if !crons.is_empty() {
105 spawn_cron_scheduler(&harness, &ports, &crons)?;
106 }
107
108 let app = harness
109 .router(clone_ports(&ports))
110 .layer(from_fn_with_state(
111 IpTrust {
112 trusted: trusted_proxy_headers(&*config),
113 },
114 client_ip_layer,
115 ));
116
117 let addr = listener
118 .local_addr()
119 .map(|addr| addr.to_string())
120 .unwrap_or_default();
121 tracing::info!(%addr, "cratefield native runtime listening");
122
123 axum::serve(
124 listener,
125 app.into_make_service_with_connect_info::<SocketAddr>(),
126 )
127 .with_graceful_shutdown(shutdown_signal())
128 .await
129 .map_err(ServeError::Serve)
130}
131
132fn cron_expressions(config: &dyn Config) -> Vec<String> {
135 config
136 .get("CRONS")
137 .map(|raw| {
138 raw.split(',')
139 .map(str::trim)
140 .filter(|entry| !entry.is_empty())
141 .map(str::to_owned)
142 .collect::<Vec<_>>()
143 })
144 .unwrap_or_default()
145}
146
147fn listen_addr(config: &dyn Config) -> Result<SocketAddr, ServeError> {
148 let raw = config
149 .get("LISTEN_ADDR")
150 .unwrap_or_else(|| DEFAULT_LISTEN_ADDR.to_owned());
151 raw.parse::<SocketAddr>()
152 .map_err(|_| ServeError::ListenAddr { addr: raw })
153}
154
155#[derive(Clone)]
158struct IpTrust {
159 trusted: Vec<String>,
160}
161
162async fn client_ip_layer(
168 State(trust): State<IpTrust>,
169 mut request: Request,
170 next: Next,
171) -> Response {
172 let peer = request
173 .extensions()
174 .get::<ConnectInfo<SocketAddr>>()
175 .map(|ConnectInfo(addr)| addr.ip());
176 let resolved = client_ip(request.headers(), &trust.trusted, peer);
177 normalize_headers(request.headers_mut(), resolved);
178 next.run(request).await
179}
180
181pub async fn shutdown_signal() {
191 let ctrl_c = tokio::signal::ctrl_c();
192 #[cfg(unix)]
193 {
194 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
195 .expect("install SIGTERM handler");
196 tokio::select! {
197 _ = ctrl_c => {},
198 _ = sigterm.recv() => {},
199 }
200 }
201 #[cfg(not(unix))]
202 {
203 if ctrl_c.await.is_err() {
204 tracing::error!("ctrl_c handler failed; exiting");
205 }
206 }
207 tracing::info!("shutdown signal received; draining in-flight requests");
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use cratefield_core::MapConfig;
214
215 #[test]
216 fn crons_default_to_empty_and_split_on_commas() {
217 assert!(cron_expressions(&MapConfig::default()).is_empty());
218 let cfg = MapConfig::from_pairs([("CRONS", "*/5 * * * * , 0 0 * * *,,")]);
219 assert_eq!(cron_expressions(&cfg), ["*/5 * * * *", "0 0 * * *"]);
220 }
221
222 #[test]
223 fn listen_addr_defaults_to_loopback() {
224 let addr = listen_addr(&MapConfig::default()).expect("default parses");
225 assert_eq!(addr.to_string(), "127.0.0.1:8080");
226
227 let cfg = MapConfig::from_pairs([("LISTEN_ADDR", "0.0.0.0:9999")]);
228 let addr = listen_addr(&cfg).expect("parses");
229 assert_eq!(addr.to_string(), "0.0.0.0:9999");
230 }
231
232 #[test]
233 fn bad_listen_addr_is_an_error() {
234 let cfg = MapConfig::from_pairs([("LISTEN_ADDR", "not-an-addr")]);
235 assert!(matches!(
236 listen_addr(&cfg),
237 Err(ServeError::ListenAddr { .. })
238 ));
239 }
240}