1use anyhow::Result;
2use log::info;
3use std::future::Future;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::time::Duration;
7use tokio::sync::RwLock;
8use warp::Filter;
9use serde::{Serialize, Deserialize};
10
11use crate::command_sender::MultiCommandSender;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct McStatusSnapshot {
16 pub online: bool,
17 pub players_online: i32,
18 pub players_max: i32,
19 pub version: String,
20 pub latency_ms: u64,
21 pub motd: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub error: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub tps: Option<f64>,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub alert: Option<String>,
30}
31
32impl McStatusSnapshot {
33 pub fn offline(error: &str) -> Self {
34 Self {
35 online: false,
36 players_online: 0,
37 players_max: 0,
38 version: String::new(),
39 latency_ms: 0,
40 motd: String::new(),
41 error: Some(error.to_string()),
42 tps: None,
43 alert: Some("offline".to_string()),
44 }
45 }
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49pub struct StatusResponse {
50 pub status: String,
51 pub uptime: u64,
52 pub mc_status: McStatusSnapshot,
53 pub rcon_available: bool,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct CommandRequest {
58 pub command: String,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62pub struct CommandResponse {
63 pub success: bool,
64 pub result: String,
65}
66
67pub struct HttpApi {
68 port: u16,
69 sender: Arc<RwLock<MultiCommandSender>>,
70 start_time: std::time::Instant,
71 pub mc_status_cache: Arc<RwLock<Option<(McStatusSnapshot, std::time::Instant)>>>,
72 rcon_available: bool,
73 mc_port: u16,
74 mc_status_config: crate::config::McStatusConfig,
75 ping_lock: AtomicBool,
76}
77
78impl HttpApi {
79 pub fn new(
80 port: u16,
81 sender: Arc<RwLock<MultiCommandSender>>,
82 rcon_available: bool,
83 mc_port: u16,
84 mc_status_config: crate::config::McStatusConfig,
85 ) -> Self {
86 Self {
87 port,
88 sender,
89 start_time: std::time::Instant::now(),
90 mc_status_cache: Arc::new(RwLock::new(None)),
91 rcon_available,
92 mc_port,
93 mc_status_config,
94 ping_lock: AtomicBool::new(false),
95 }
96 }
97
98 pub async fn fetch_mc_status(&self) -> McStatusSnapshot {
99 {
101 let cache = self.mc_status_cache.read().await;
102 if let Some((ref snapshot, ref time)) = *cache {
103 let age = time.elapsed().as_secs();
104 if age < self.mc_status_config.ping_interval_secs {
105 return snapshot.clone();
106 }
107 }
108 }
109
110 if self.ping_lock.swap(true, Ordering::Acquire) {
112 let cache = self.mc_status_cache.read().await;
113 if let Some((ref snapshot, _)) = *cache {
114 return snapshot.clone();
115 }
116 return McStatusSnapshot::offline("status probe busy, retry later");
117 }
118
119 struct PingGuard<'a>(&'a AtomicBool);
121 impl<'a> Drop for PingGuard<'a> {
122 fn drop(&mut self) {
123 self.0.store(false, Ordering::Release);
124 }
125 }
126 let _guard = PingGuard(&self.ping_lock);
127
128 let timeout = Duration::from_secs(self.mc_status_config.ping_timeout_secs);
129 let result = mc_status_probe::ping("127.0.0.1", self.mc_port, timeout, None).await;
130
131 let snapshot = match result {
134 Ok(r) => McStatusSnapshot {
135 online: true,
136 players_online: r.players_online,
137 players_max: r.players_max,
138 version: r.version_name,
139 latency_ms: r.latency_ms,
140 motd: r.description,
141 error: None,
142 tps: None,
143 alert: None,
144 },
145 Err(e) => McStatusSnapshot::offline(&e.to_string()),
146 };
147
148 {
150 let mut cache = self.mc_status_cache.write().await;
151 *cache = Some((snapshot.clone(), std::time::Instant::now()));
152 }
153
154 snapshot
155 }
156
157 pub async fn start<S>(self: Arc<Self>, shutdown: S) -> Result<()>
158 where
159 S: Future<Output = ()> + Send + 'static,
160 {
161 let start_time = self.start_time;
162 let preferred_port = self.port;
163 let cache = self.mc_status_cache.clone();
164 let rcon_available = self.rcon_available;
165
166 let status_cache = cache.clone();
168 let status_route = warp::path("status")
169 .and(warp::get())
170 .and_then(move || {
171 let cache = status_cache.clone();
172 let st = start_time;
173 let rcon_ok = rcon_available;
174 async move {
175 let mc_status = {
176 let cached = cache.read().await;
177 match *cached {
178 Some((ref snapshot, _)) => snapshot.clone(),
179 None => McStatusSnapshot::offline("no status data yet"),
180 }
181 };
182 let response = StatusResponse {
183 status: "running".to_string(),
184 uptime: st.elapsed().as_secs(),
185 mc_status,
186 rcon_available: rcon_ok,
187 };
188 Ok::<_, warp::Rejection>(warp::reply::json(&response))
189 }
190 });
191
192 let sender = self.sender.clone();
194 let command_route = warp::path("command")
195 .and(warp::post())
196 .and(warp::body::json())
197 .and_then(move |req: CommandRequest| {
198 let sender = sender.clone();
199 let rcon_ok = rcon_available;
200 async move {
201 if !rcon_ok {
202 let response = CommandResponse {
203 success: false,
204 result: "RCON is not configured or not available".to_string(),
205 };
206 return Ok(warp::reply::json(&response));
207 }
208 let mut sender_guard = sender.write().await;
209 match sender_guard.send_command(&req.command).await {
210 Ok(response_text) => {
211 let response = CommandResponse {
212 success: true,
213 result: response_text.trim().to_string(),
214 };
215 Ok::<_, warp::Rejection>(warp::reply::json(&response))
216 }
217 Err(e) => {
218 let response = CommandResponse {
219 success: false,
220 result: e.to_string(),
221 };
222 Ok(warp::reply::json(&response))
223 }
224 }
225 }
226 });
227
228 let routes = status_route
229 .or(command_route)
230 .with(warp::cors().allow_any_origin());
231
232 info!("Starting HTTP API server on port {}", preferred_port);
233
234 warp::serve(routes)
235 .bind_with_graceful_shutdown(([0, 0, 0, 0], preferred_port), async { shutdown.await })
236 .1
237 .await;
238
239 Ok(())
240 }
241}