1use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9
10use futures_util::{SinkExt, StreamExt};
11use log::{debug, error, info, warn};
12use tokio::net::{TcpListener, TcpStream};
13use tokio::sync::{broadcast, RwLock};
14use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
15use tokio_tungstenite::tungstenite::protocol::CloseFrame;
16use tokio_tungstenite::tungstenite::Message;
17use tokio_util::sync::CancellationToken;
18
19use crate::handlers::{GetSlotValuesHandler, OnClientConnectedHandler};
20use arora_types::value::Value;
21
22use crate::messages::{Incoming, Outgoing};
23use crate::registry::Registry;
24
25pub type SetSlotValuesHandler =
30 Arc<dyn Fn(HashMap<String, Value>) -> Result<(), String> + Send + Sync>;
31
32#[derive(Clone)]
34pub struct ServerConfig {
35 pub port: u16,
37 pub bind_address: String,
41 pub validate_paths: bool,
43 pub serve_control_panel: bool,
45}
46
47impl Default for ServerConfig {
48 fn default() -> Self {
49 Self {
50 port: 9000,
51 bind_address: "127.0.0.1".to_string(),
52 validate_paths: true,
53 serve_control_panel: false,
54 }
55 }
56}
57
58impl ServerConfig {
59 pub fn with_port(port: u16) -> Self {
61 Self {
62 port,
63 ..Default::default()
64 }
65 }
66
67 pub fn bind_address(mut self, addr: impl Into<String>) -> Self {
69 self.bind_address = addr.into();
70 self
71 }
72
73 pub fn validate_paths(mut self, validate: bool) -> Self {
75 self.validate_paths = validate;
76 self
77 }
78
79 pub fn serve_control_panel(mut self, enable: bool) -> Self {
81 self.serve_control_panel = enable;
82 self
83 }
84}
85
86pub struct AroraWSServer {
92 config: ServerConfig,
93 registry: Arc<Registry>,
94 set_slot_values_handler: RwLock<Option<SetSlotValuesHandler>>,
95 get_slot_values_handler: RwLock<Option<GetSlotValuesHandler>>,
96 on_client_connected_handler: RwLock<Option<OnClientConnectedHandler>>,
97 active_client: Arc<RwLock<Option<CancellationToken>>>,
99 is_running: RwLock<bool>,
100 outbound_tx: broadcast::Sender<Outgoing>,
102}
103
104impl AroraWSServer {
105 pub fn new(config: ServerConfig) -> Self {
107 Self {
108 config,
109 registry: Arc::new(Registry::new()),
110 set_slot_values_handler: RwLock::new(None),
111 get_slot_values_handler: RwLock::new(None),
112 on_client_connected_handler: RwLock::new(None),
113 active_client: Arc::new(RwLock::new(None)),
114 is_running: RwLock::new(false),
115 outbound_tx: broadcast::channel(256).0,
116 }
117 }
118
119 pub fn with_port(port: u16) -> Self {
121 Self::new(ServerConfig::with_port(port))
122 }
123
124 pub fn push(&self, msg: Outgoing) {
126 let _ = self.outbound_tx.send(msg);
127 }
128
129 pub fn subscribe(&self) -> broadcast::Receiver<Outgoing> {
131 self.outbound_tx.subscribe()
132 }
133
134 pub fn registry(&self) -> &Arc<Registry> {
136 &self.registry
137 }
138
139 pub async fn set_set_slot_values_handler<F>(&self, handler: F)
142 where
143 F: Fn(HashMap<String, Value>) -> Result<(), String> + Send + Sync + 'static,
144 {
145 *self.set_slot_values_handler.write().await = Some(Arc::new(handler));
146 }
147
148 pub async fn set_get_slot_values_handler(&self, handler: GetSlotValuesHandler) {
151 *self.get_slot_values_handler.write().await = Some(handler);
152 }
153
154 pub async fn set_on_client_connected_handler(&self, handler: OnClientConnectedHandler) {
156 *self.on_client_connected_handler.write().await = Some(handler);
157 }
158
159 pub async fn disconnect_client(&self) {
161 let mut guard = self.active_client.write().await;
162 if let Some(token) = guard.take() {
163 token.cancel();
164 info!(
165 "Disconnected active client on ws://{}:{}",
166 self.config.bind_address, self.config.port
167 );
168 }
169 }
170
171 pub async fn is_running(&self) -> bool {
173 *self.is_running.read().await
174 }
175
176 pub fn port(&self) -> u16 {
178 self.config.port
179 }
180
181 pub async fn run(&self, cancel_token: CancellationToken) -> Result<(), String> {
183 let addr = format!("{}:{}", self.config.bind_address, self.config.port);
184 let listener = TcpListener::bind(&addr)
185 .await
186 .map_err(|e| format!("Failed to bind to {}: {}", addr, e))?;
187
188 info!("Arora WebSocket server listening on ws://{}", addr);
189 if self.config.serve_control_panel {
190 info!("Control panel available at http://{}", addr);
191 }
192 *self.is_running.write().await = true;
193
194 let serve_control_panel = self.config.serve_control_panel;
195 let validate_paths = self.config.validate_paths;
196 let conn_id = self.connection_id();
197 let bind_addr = self.config.bind_address.clone();
198 let port = self.config.port;
199
200 let set_handler = self.set_slot_values_handler.read().await.clone();
202 let get_handler = self.get_slot_values_handler.read().await.clone();
203 let on_connected = self.on_client_connected_handler.read().await.clone();
204
205 loop {
206 tokio::select! {
207 result = listener.accept() => {
208 match result {
209 Ok((stream, peer_addr)) => {
210 let active_client = self.active_client.clone();
213 let registry = self.registry.clone();
214 let set_handler = set_handler.clone();
215 let get_handler = get_handler.clone();
216 let on_connected = on_connected.clone();
217 let conn_id = conn_id.clone();
218 let bind_addr = bind_addr.clone();
219 let parent_token = cancel_token.clone();
220 let outbound_tx = self.outbound_tx.clone();
221
222 tokio::spawn(async move {
223 let is_ws_upgrade = {
225 let mut peek_buf = [0u8; 4096];
226 match tokio::time::timeout(
227 std::time::Duration::from_secs(5),
228 stream.peek(&mut peek_buf),
229 ).await {
230 Ok(Ok(n)) => {
231 let req = String::from_utf8_lossy(&peek_buf[..n]);
232 let lower = req.to_ascii_lowercase();
233 lower.contains("upgrade") && lower.contains("websocket")
234 }
235 Ok(Err(e)) => {
236 error!("Failed to peek connection from {}: {}", peer_addr, e);
237 return;
238 }
239 Err(_) => {
240 debug!("Connection from {} sent no data within timeout", peer_addr);
241 return;
242 }
243 }
244 };
245
246 if !is_ws_upgrade {
247 if serve_control_panel {
248 serve_control_panel_http(stream).await;
249 }
250 return;
251 }
252
253 let client_token = parent_token.child_token();
255 {
256 let mut guard = active_client.write().await;
257 if let Some(old) = guard.take() {
258 old.cancel();
259 info!("Disconnected active client on ws://{}:{}", bind_addr, port);
260 }
261 *guard = Some(client_token.clone());
262 }
263
264 if let Some(ref handler) = on_connected {
266 handler(conn_id);
267 }
268
269 handle_connection(
270 stream, peer_addr, registry,
271 set_handler, get_handler,
272 validate_paths, client_token, active_client,
273 outbound_tx,
274 ).await;
275 });
276 }
277 Err(e) => {
278 error!("Failed to accept connection: {}", e);
279 }
280 }
281 }
282 _ = cancel_token.cancelled() => {
283 info!("Arora WebSocket server shutting down");
284 self.disconnect_client().await;
286 break;
287 }
288 }
289 }
290
291 *self.is_running.write().await = false;
292 Ok(())
293 }
294
295 pub fn connection_id(&self) -> String {
297 format!("ws://127.0.0.1:{}", self.config.port)
298 }
299}
300
301#[allow(clippy::too_many_arguments)]
303async fn handle_connection(
304 stream: TcpStream,
305 addr: SocketAddr,
306 registry: Arc<Registry>,
307 set_slot_values_handler: Option<SetSlotValuesHandler>,
308 get_slot_values_handler: Option<GetSlotValuesHandler>,
309 validate_paths: bool,
310 client_token: CancellationToken,
311 active_client: Arc<RwLock<Option<CancellationToken>>>,
312 outbound_tx: broadcast::Sender<Outgoing>,
313) {
314 info!("New WebSocket connection from: {}", addr);
315
316 let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
317 max_message_size: Some(1 << 20),
320 max_frame_size: Some(256 << 10),
321 ..Default::default()
322 };
323 let ws_stream = match tokio_tungstenite::accept_async_with_config(stream, Some(ws_config)).await
324 {
325 Ok(ws) => ws,
326 Err(e) => {
327 error!("Error during WebSocket handshake: {}", e);
328 return;
329 }
330 };
331
332 let (mut write, mut read) = ws_stream.split();
333 let mut outbound_rx = outbound_tx.subscribe();
334
335 loop {
336 tokio::select! {
337 msg = read.next() => {
338 match msg {
339 Some(Ok(Message::Text(text))) => {
340 debug!("Received message: {}", text);
341
342 let response = match serde_json::from_str::<Incoming>(&text) {
343 Ok(incoming) => {
344 process_message(incoming, ®istry, &set_slot_values_handler, &get_slot_values_handler, validate_paths).await
345 }
346 Err(e) => {
347 warn!("Failed to parse message: {}", e);
348 Outgoing::Error {
349 request_id: None,
350 message: format!("Invalid message format: {}", e),
351 }
352 }
353 };
354
355 let response_text = match serde_json::to_string(&response) {
356 Ok(text) => text,
357 Err(e) => {
358 error!("Failed to serialize response: {}", e);
359 break;
360 }
361 };
362 if let Err(e) = write.send(Message::Text(response_text)).await {
363 error!("Failed to send response: {}", e);
364 break;
365 }
366 }
367 Some(Ok(Message::Close(_))) => {
368 info!("Client {} disconnected", addr);
369 break;
370 }
371 Some(Ok(Message::Ping(data))) => {
372 if let Err(e) = write.send(Message::Pong(data)).await {
373 error!("Failed to send pong: {}", e);
374 break;
375 }
376 }
377 Some(Ok(_)) => {
378 }
380 Some(Err(e)) => {
381 error!("Error reading message: {}", e);
382 break;
383 }
384 None => {
385 break;
387 }
388 }
389 }
390 pushed = outbound_rx.recv() => {
391 match pushed {
392 Ok(msg) => {
393 let text = match serde_json::to_string(&msg) {
394 Ok(text) => text,
395 Err(e) => {
396 error!("Failed to serialize push message: {}", e);
397 continue;
398 }
399 };
400 if let Err(e) = write.send(Message::Text(text)).await {
401 error!("Failed to push message: {}", e);
402 break;
403 }
404 }
405 Err(broadcast::error::RecvError::Closed) => break,
406 Err(broadcast::error::RecvError::Lagged(_)) => {}
407 }
408 }
409 _ = client_token.cancelled() => {
410 info!("Client {} disconnected by server (exclusive client policy)", addr);
411 let close_frame = CloseFrame {
413 code: CloseCode::Normal,
414 reason: "Another client connected".into(),
415 };
416 let _ = write.send(Message::Close(Some(close_frame))).await;
417 break;
418 }
419 }
420 }
421
422 if !client_token.is_cancelled() {
425 let mut guard = active_client.write().await;
426 *guard = None;
427 }
428
429 info!("Connection closed for: {}", addr);
430}
431
432async fn serve_control_panel_http(mut stream: TcpStream) {
434 use tokio::io::{AsyncReadExt, AsyncWriteExt};
435
436 const HTML: &str = include_str!("control_panel.html");
437
438 let mut buf = vec![0u8; 4096];
440 let _ = stream.read(&mut buf).await;
441
442 let body = HTML.as_bytes();
443 let header = format!(
444 "HTTP/1.1 200 OK\r\n\
445 Content-Type: text/html; charset=utf-8\r\n\
446 Content-Length: {}\r\n\
447 Connection: close\r\n\
448 \r\n",
449 body.len()
450 );
451
452 let _ = stream.write_all(header.as_bytes()).await;
453 let _ = stream.write_all(body).await;
454}
455
456pub async fn process_message(
461 incoming: Incoming,
462 registry: &Registry,
463 set_slot_values_handler: &Option<SetSlotValuesHandler>,
464 get_slot_values_handler: &Option<GetSlotValuesHandler>,
465 validate_paths: bool,
466) -> Outgoing {
467 match incoming {
468 Incoming::SetSlotValues { values } => {
469 if validate_paths {
471 let input_paths = registry.get_input_paths().await;
472 let invalid_paths: Vec<&str> = values
473 .keys()
474 .filter(|path| !input_paths.iter().any(|p| p == *path))
475 .map(|s| s.as_str())
476 .collect();
477
478 if !invalid_paths.is_empty() {
479 warn!("Invalid paths in SetSlotValues: {:?}", invalid_paths);
480 return Outgoing::SetSlotValuesResp {
481 success: false,
482 message: Some(format!(
483 "Unknown input path(s): {}",
484 invalid_paths.join(", ")
485 )),
486 };
487 }
488 }
489
490 if let Some(handler) = set_slot_values_handler {
492 match handler(values) {
493 Ok(()) => {
494 debug!("SetSlotValues handled successfully");
495 Outgoing::SetSlotValuesResp {
496 success: true,
497 message: None,
498 }
499 }
500 Err(e) => {
501 error!("SetSlotValues handler error: {}", e);
502 Outgoing::SetSlotValuesResp {
503 success: false,
504 message: Some(e),
505 }
506 }
507 }
508 } else {
509 debug!("No SetSlotValues handler registered, acknowledging");
511 Outgoing::SetSlotValuesResp {
512 success: true,
513 message: None,
514 }
515 }
516 }
517
518 Incoming::GetSlotValues { slots } => {
519 if let Some(handler) = get_slot_values_handler {
521 let values = handler(slots).await;
522 Outgoing::GetSlotValuesResp { values }
523 } else {
524 debug!("No GetSlotValues handler registered, returning empty values");
526 Outgoing::GetSlotValuesResp {
527 values: HashMap::new(),
528 }
529 }
530 }
531
532 Incoming::ListSlots { path } => {
533 let slots = registry.get_slots_filtered(path.as_deref()).await;
534 Outgoing::ListSlotsResp { slots }
535 }
536
537 Incoming::ListMethods { path } => {
538 let methods = registry.get_methods_filtered(path.as_deref()).await;
539 Outgoing::ListMethodsResp { methods }
540 }
541
542 Incoming::Invoke {
543 method,
544 args,
545 request_id,
546 } => {
547 let result = registry.invoke_method(&method, args).await;
548 Outgoing::InvokeResp {
549 success: result.success,
550 request_id,
551 value: result.value,
552 message: result.message,
553 }
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_server_config_default() {
564 let config = ServerConfig::default();
565 assert_eq!(config.port, 9000);
566 assert_eq!(config.bind_address, "127.0.0.1");
567 assert!(config.validate_paths);
568 assert!(!config.serve_control_panel);
569 }
570
571 #[test]
572 fn test_server_config_builder() {
573 let config = ServerConfig::with_port(8080)
574 .bind_address("127.0.0.1")
575 .validate_paths(false)
576 .serve_control_panel(true);
577
578 assert_eq!(config.port, 8080);
579 assert_eq!(config.bind_address, "127.0.0.1");
580 assert!(!config.validate_paths);
581 assert!(config.serve_control_panel);
582 }
583}