manabrew_server/
server.rs1use 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}