Skip to main content

manabrew_server/
server.rs

1use std::net::SocketAddr;
2use std::sync::Arc;
3use std::time::Duration;
4
5use axum::extract::State;
6use axum::{response::Json, routing::get, Router};
7use metrics_exporter_prometheus::PrometheusHandle;
8use serde_json::{json, Value};
9use socket2::{SockRef, TcpKeepalive};
10use tokio::net::TcpListener;
11use tokio::sync::Notify;
12use tokio_tungstenite::tungstenite::Message;
13use tracing::{debug, error, info, warn};
14
15use crate::cleanup::cleanup_loop;
16use crate::connection::handle_connection;
17use crate::protocol::ServerMessage;
18use crate::state::ServerState;
19
20const SHUTDOWN_GRACE: Duration = Duration::from_secs(10);
21const SHUTDOWN_RECONNECT_HINT_S: u32 = 5;
22
23pub async fn run_server(
24    state: Arc<ServerState>,
25    addr: SocketAddr,
26    health_addr: SocketAddr,
27    metrics_handle: PrometheusHandle,
28) {
29    let listener = TcpListener::bind(addr)
30        .await
31        .expect("Failed to bind to address");
32
33    let shutdown = Arc::new(Notify::new());
34    tokio::spawn(wait_for_shutdown_signal(shutdown.clone()));
35
36    serve(state, listener, health_addr, metrics_handle, shutdown).await;
37}
38
39pub async fn serve(
40    state: Arc<ServerState>,
41    listener: TcpListener,
42    health_addr: SocketAddr,
43    metrics_handle: PrometheusHandle,
44    shutdown: Arc<Notify>,
45) {
46    if let Ok(addr) = listener.local_addr() {
47        info!("[server] Forge Server listening on ws://{}", addr);
48    }
49    info!("[server] Server key: {}", mask_key(&state.server_key));
50    info!("[server] Max rooms: {}", state.max_rooms);
51
52    tokio::spawn(cleanup_loop(state.clone()));
53    tokio::spawn(run_health_listener(
54        state.clone(),
55        health_addr,
56        metrics_handle,
57    ));
58
59    let accept = accept_loop(state.clone(), listener, shutdown.clone());
60
61    tokio::select! {
62        _ = accept => {}
63        _ = shutdown.notified() => {
64            info!("[server] shutdown signal received — draining");
65        }
66    }
67
68    drain_and_exit(state).await;
69}
70
71async fn accept_loop(state: Arc<ServerState>, listener: TcpListener, shutdown: Arc<Notify>) {
72    loop {
73        tokio::select! {
74            res = listener.accept() => match res {
75                Ok((stream, peer_addr)) => {
76                    debug!("[server] accepted connection from {}", peer_addr);
77                    apply_tcp_socket_options(&stream, peer_addr);
78                    let state = state.clone();
79                    tokio::spawn(async move {
80                        if let Err(e) = handle_connection(stream, peer_addr, state).await {
81                            error!("[server] connection error: {}", e);
82                        }
83                    });
84                }
85                Err(e) => {
86                    error!("[server] accept error: {}", e);
87                }
88            },
89            _ = shutdown.notified() => {
90                info!("[server] accept loop stopping");
91                return;
92            }
93        }
94    }
95}
96
97fn apply_tcp_socket_options(stream: &tokio::net::TcpStream, peer_addr: SocketAddr) {
98    if let Err(e) = stream.set_nodelay(true) {
99        warn!("[server] failed to disable Nagle on {}: {}", peer_addr, e);
100    }
101    let keepalive = TcpKeepalive::new()
102        .with_time(Duration::from_secs(30))
103        .with_interval(Duration::from_secs(10));
104    if let Err(e) = SockRef::from(stream).set_tcp_keepalive(&keepalive) {
105        warn!(
106            "[server] failed to enable TCP keepalive on {}: {}",
107            peer_addr, e
108        );
109    }
110}
111
112async fn wait_for_shutdown_signal(shutdown: Arc<Notify>) {
113    #[cfg(unix)]
114    {
115        use tokio::signal::unix::{signal, SignalKind};
116        let mut term = match signal(SignalKind::terminate()) {
117            Ok(s) => s,
118            Err(e) => {
119                warn!("[server] failed to install SIGTERM handler: {}", e);
120                return;
121            }
122        };
123        let mut int = match signal(SignalKind::interrupt()) {
124            Ok(s) => s,
125            Err(e) => {
126                warn!("[server] failed to install SIGINT handler: {}", e);
127                return;
128            }
129        };
130        tokio::select! {
131            _ = term.recv() => info!("[server] SIGTERM received"),
132            _ = int.recv()  => info!("[server] SIGINT received"),
133        }
134    }
135    #[cfg(not(unix))]
136    {
137        if let Err(e) = tokio::signal::ctrl_c().await {
138            warn!("[server] ctrl_c handler error: {}", e);
139            return;
140        }
141        info!("[server] ctrl_c received");
142    }
143    shutdown.notify_waiters();
144}
145
146async fn drain_and_exit(state: Arc<ServerState>) {
147    let msg = ServerMessage::ServerShuttingDown {
148        reconnect_in_s: SHUTDOWN_RECONNECT_HINT_S,
149    };
150    let json = match serde_json::to_string(&msg) {
151        Ok(s) => s,
152        Err(e) => {
153            error!("[server] failed to serialize shutdown message: {}", e);
154            return;
155        }
156    };
157
158    let mut notified = 0usize;
159    for entry in state.players.iter() {
160        let player = entry.value();
161        if !player.connected {
162            continue;
163        }
164        if player.sender.send(Message::Text(json.clone())).is_ok() {
165            notified += 1;
166        }
167    }
168    info!(
169        "[server] broadcast ServerShuttingDown to {} players",
170        notified
171    );
172
173    tokio::time::sleep(SHUTDOWN_GRACE).await;
174    info!("[server] shutdown grace elapsed — exiting");
175}
176
177async fn run_health_listener(
178    state: Arc<ServerState>,
179    addr: SocketAddr,
180    metrics_handle: PrometheusHandle,
181) {
182    let app = Router::new()
183        .route("/health", get(health_handler))
184        .route("/metrics", get(metrics_handler))
185        .with_state((state, metrics_handle));
186
187    let listener = match TcpListener::bind(addr).await {
188        Ok(l) => l,
189        Err(e) => {
190            error!("[server] failed to bind health listener on {}: {}", addr, e);
191            return;
192        }
193    };
194    info!("[server] health endpoint on http://{}/health", addr);
195
196    if let Err(e) = axum::serve(listener, app).await {
197        error!("[server] health listener stopped: {}", e);
198    }
199}
200
201async fn health_handler(
202    State((state, _)): State<(Arc<ServerState>, PrometheusHandle)>,
203) -> Json<Value> {
204    let connected_players = state.players.iter().filter(|e| e.value().connected).count();
205    Json(json!({
206        "status": "ok",
207        "rooms": state.rooms.len(),
208        "connected_players": connected_players,
209        "uptime_s": uptime_secs(),
210    }))
211}
212
213async fn metrics_handler(
214    State((state, metrics_handle)): State<(Arc<ServerState>, PrometheusHandle)>,
215) -> String {
216    crate::metrics::refresh_gauges(&state);
217    metrics_handle.run_upkeep();
218    metrics_handle.render()
219}
220
221fn uptime_secs() -> u64 {
222    use std::sync::OnceLock;
223    use std::time::Instant;
224    static START: OnceLock<Instant> = OnceLock::new();
225    START.get_or_init(Instant::now).elapsed().as_secs()
226}
227
228fn mask_key(key: &str) -> String {
229    if key.len() <= 2 {
230        "*".repeat(key.len())
231    } else {
232        format!(
233            "{}{}{}",
234            &key[..1],
235            "*".repeat(key.len() - 2),
236            &key[key.len() - 1..]
237        )
238    }
239}