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_keepalive(&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 from {}: {}", peer_addr, 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_keepalive(stream: &tokio::net::TcpStream, peer_addr: SocketAddr) {
98    let keepalive = TcpKeepalive::new()
99        .with_time(Duration::from_secs(30))
100        .with_interval(Duration::from_secs(10));
101    if let Err(e) = SockRef::from(stream).set_tcp_keepalive(&keepalive) {
102        warn!(
103            "[server] failed to enable TCP keepalive on {}: {}",
104            peer_addr, e
105        );
106    }
107}
108
109async fn wait_for_shutdown_signal(shutdown: Arc<Notify>) {
110    #[cfg(unix)]
111    {
112        use tokio::signal::unix::{signal, SignalKind};
113        let mut term = match signal(SignalKind::terminate()) {
114            Ok(s) => s,
115            Err(e) => {
116                warn!("[server] failed to install SIGTERM handler: {}", e);
117                return;
118            }
119        };
120        let mut int = match signal(SignalKind::interrupt()) {
121            Ok(s) => s,
122            Err(e) => {
123                warn!("[server] failed to install SIGINT handler: {}", e);
124                return;
125            }
126        };
127        tokio::select! {
128            _ = term.recv() => info!("[server] SIGTERM received"),
129            _ = int.recv()  => info!("[server] SIGINT received"),
130        }
131    }
132    #[cfg(not(unix))]
133    {
134        if let Err(e) = tokio::signal::ctrl_c().await {
135            warn!("[server] ctrl_c handler error: {}", e);
136            return;
137        }
138        info!("[server] ctrl_c received");
139    }
140    shutdown.notify_waiters();
141}
142
143async fn drain_and_exit(state: Arc<ServerState>) {
144    let msg = ServerMessage::ServerShuttingDown {
145        reconnect_in_s: SHUTDOWN_RECONNECT_HINT_S,
146    };
147    let json = match serde_json::to_string(&msg) {
148        Ok(s) => s,
149        Err(e) => {
150            error!("[server] failed to serialize shutdown message: {}", e);
151            return;
152        }
153    };
154
155    let mut notified = 0usize;
156    for entry in state.players.iter() {
157        let player = entry.value();
158        if !player.connected {
159            continue;
160        }
161        if player.sender.send(Message::Text(json.clone())).is_ok() {
162            notified += 1;
163        }
164    }
165    info!(
166        "[server] broadcast ServerShuttingDown to {} players",
167        notified
168    );
169
170    tokio::time::sleep(SHUTDOWN_GRACE).await;
171    info!("[server] shutdown grace elapsed — exiting");
172}
173
174async fn run_health_listener(
175    state: Arc<ServerState>,
176    addr: SocketAddr,
177    metrics_handle: PrometheusHandle,
178) {
179    let app = Router::new()
180        .route("/health", get(health_handler))
181        .route("/metrics", get(metrics_handler))
182        .with_state((state, metrics_handle));
183
184    let listener = match TcpListener::bind(addr).await {
185        Ok(l) => l,
186        Err(e) => {
187            error!("[server] failed to bind health listener on {}: {}", addr, e);
188            return;
189        }
190    };
191    info!("[server] health endpoint on http://{}/health", addr);
192
193    if let Err(e) = axum::serve(listener, app).await {
194        error!("[server] health listener stopped: {}", e);
195    }
196}
197
198async fn health_handler(
199    State((state, _)): State<(Arc<ServerState>, PrometheusHandle)>,
200) -> Json<Value> {
201    let connected_players = state.players.iter().filter(|e| e.value().connected).count();
202    Json(json!({
203        "status": "ok",
204        "rooms": state.rooms.len(),
205        "connected_players": connected_players,
206        "uptime_s": uptime_secs(),
207    }))
208}
209
210async fn metrics_handler(
211    State((state, metrics_handle)): State<(Arc<ServerState>, PrometheusHandle)>,
212) -> String {
213    crate::metrics::refresh_gauges(&state);
214    metrics_handle.run_upkeep();
215    metrics_handle.render()
216}
217
218fn uptime_secs() -> u64 {
219    use std::sync::OnceLock;
220    use std::time::Instant;
221    static START: OnceLock<Instant> = OnceLock::new();
222    START.get_or_init(Instant::now).elapsed().as_secs()
223}
224
225fn mask_key(key: &str) -> String {
226    if key.len() <= 2 {
227        "*".repeat(key.len())
228    } else {
229        format!(
230            "{}{}{}",
231            &key[..1],
232            "*".repeat(key.len() - 2),
233            &key[key.len() - 1..]
234        )
235    }
236}