server_socket.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. """
  2. Server Socket Implementation for Trixy Network
  3. This module provides server-side socket handling for the Trixy system.
  4. It implements:
  5. - Multi-threaded server socket management
  6. - Client connection handling and authentication
  7. - Command processing and response handling
  8. - Audio stream server management
  9. - Connection monitoring and health checking
  10. - Integration with satellite registration process
  11. The server supports multiple simultaneous client connections and handles
  12. the full satellite connection workflow as specified in CLAUDE.md.
  13. """
  14. import socket
  15. import threading
  16. import time
  17. import uuid
  18. import json
  19. from typing import Dict, Any, Optional, List, Tuple, Callable
  20. from dataclasses import dataclass, field
  21. from concurrent.futures import ThreadPoolExecutor
  22. import select
  23. from .protocol import TrixyProtocol, TrixyMessage, MessageFlags
  24. from .cmd import (
  25. TrixyCommand,
  26. SatelliteHelloCommand,
  27. SatelliteRegistrationCommand,
  28. SatelliteConnectionCommand,
  29. create_hello_response,
  30. create_registration_response,
  31. create_connection_response
  32. )
  33. def pprint(message: str) -> None:
  34. """Server socket logging function."""
  35. print(f"[SERVER_SOCKET] {message}")
  36. class ServerError(Exception):
  37. """Base exception for server errors."""
  38. pass
  39. class PortInUseError(ServerError):
  40. """Raised when a port is already in use."""
  41. pass
  42. class ClientConnectionError(ServerError):
  43. """Raised when client connection fails."""
  44. pass
  45. @dataclass
  46. class ClientConnection:
  47. """
  48. Represents a client connection to the server.
  49. Tracks connection state, authentication status, and statistics.
  50. """
  51. connection_id: str
  52. socket: socket.socket
  53. address: Tuple[str, int]
  54. connected_time: float = field(default_factory=time.time)
  55. last_activity: float = field(default_factory=time.time)
  56. # Connection state
  57. authenticated: bool = False
  58. satellite_id: Optional[str] = None
  59. satellite_info: Dict[str, Any] = field(default_factory=dict)
  60. # Statistics
  61. bytes_sent: int = 0
  62. bytes_received: int = 0
  63. commands_sent: int = 0
  64. commands_received: int = 0
  65. # Audio streaming
  66. audio_streams: Dict[str, int] = field(default_factory=dict) # stream_type -> port
  67. def update_activity(self, bytes_delta: int = 0):
  68. """Update last activity time."""
  69. self.last_activity = time.time()
  70. if bytes_delta > 0:
  71. self.bytes_received += bytes_delta
  72. def is_timeout(self, timeout_seconds: float = 300.0) -> bool:
  73. """Check if connection has timed out."""
  74. return time.time() - self.last_activity > timeout_seconds
  75. def get_connection_info(self) -> Dict[str, Any]:
  76. """Get connection information."""
  77. return {
  78. 'connection_id': self.connection_id,
  79. 'address': self.address,
  80. 'connected_time': self.connected_time,
  81. 'last_activity': self.last_activity,
  82. 'authenticated': self.authenticated,
  83. 'satellite_id': self.satellite_id,
  84. 'satellite_info': self.satellite_info,
  85. 'bytes_sent': self.bytes_sent,
  86. 'bytes_received': self.bytes_received,
  87. 'commands_sent': self.commands_sent,
  88. 'commands_received': self.commands_received,
  89. 'audio_streams': self.audio_streams
  90. }
  91. class ServerSocketHandler:
  92. """
  93. Handles server-side socket operations.
  94. This class manages the command socket server and handles client connections,
  95. authentication, and message processing.
  96. """
  97. def __init__(self, application_container):
  98. """
  99. Initialize the server socket handler.
  100. Args:
  101. application_container: Main application container
  102. """
  103. self.application = application_container
  104. self.protocol = TrixyProtocol()
  105. # Server state
  106. self._running = False
  107. self._server_socket: Optional[socket.socket] = None
  108. self._listen_port = 2101
  109. # Connection management
  110. self._connections: Dict[str, ClientConnection] = {}
  111. self._connections_lock = threading.RLock()
  112. # Threading
  113. self._thread_pool = ThreadPoolExecutor(max_workers=50)
  114. self._accept_thread: Optional[threading.Thread] = None
  115. self._cleanup_thread: Optional[threading.Thread] = None
  116. # Registration mode
  117. self._registration_mode = False
  118. self._registration_timeout = 60.0
  119. self._registration_timer: Optional[threading.Timer] = None
  120. # Message handlers
  121. self._message_handlers: Dict[str, Callable] = {}
  122. self._setup_default_handlers()
  123. # Statistics
  124. self._stats = {
  125. 'connections_accepted': 0,
  126. 'connections_rejected': 0,
  127. 'messages_processed': 0,
  128. 'errors': 0
  129. }
  130. pprint("ServerSocketHandler initialized")
  131. def start(self, port: int = 2101) -> None:
  132. """
  133. Start the server socket.
  134. Args:
  135. port: Port to listen on
  136. Raises:
  137. ServerError: If server cannot be started
  138. """
  139. if self._running:
  140. raise ServerError("Server already running")
  141. self._listen_port = port
  142. try:
  143. # Create server socket
  144. self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  145. self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  146. self._server_socket.bind(('', port))
  147. self._server_socket.listen(50)
  148. self._running = True
  149. # Start accept thread
  150. self._accept_thread = threading.Thread(target=self._accept_connections, daemon=True)
  151. self._accept_thread.start()
  152. # Start cleanup thread
  153. self._cleanup_thread = threading.Thread(target=self._cleanup_connections, daemon=True)
  154. self._cleanup_thread.start()
  155. pprint(f"Server started on port {port}")
  156. except OSError as e:
  157. if e.errno == 98: # Address already in use
  158. raise PortInUseError(f"Port {port} is already in use") from e
  159. raise ServerError(f"Failed to start server: {e}") from e
  160. def stop(self) -> None:
  161. """Stop the server socket."""
  162. if not self._running:
  163. return
  164. pprint("Stopping server...")
  165. self._running = False
  166. # Stop registration mode
  167. if self._registration_mode:
  168. self.exit_registration_mode()
  169. # Close server socket
  170. if self._server_socket:
  171. self._server_socket.close()
  172. self._server_socket = None
  173. # Close all client connections
  174. with self._connections_lock:
  175. for connection in list(self._connections.values()):
  176. self._close_connection(connection.connection_id)
  177. # Wait for threads
  178. if self._accept_thread and self._accept_thread.is_alive():
  179. self._accept_thread.join(timeout=5.0)
  180. if self._cleanup_thread and self._cleanup_thread.is_alive():
  181. self._cleanup_thread.join(timeout=5.0)
  182. # Shutdown thread pool
  183. self._thread_pool.shutdown(wait=True)
  184. pprint("Server stopped")
  185. def enter_registration_mode(self, timeout_seconds: float = 60.0) -> None:
  186. """
  187. Enter registration mode to accept new satellites.
  188. Args:
  189. timeout_seconds: Registration timeout
  190. """
  191. if self._registration_mode:
  192. return
  193. pprint(f"Entering registration mode (timeout: {timeout_seconds}s)")
  194. self._registration_mode = True
  195. self._registration_timeout = timeout_seconds
  196. # Set timeout timer
  197. self._registration_timer = threading.Timer(
  198. timeout_seconds,
  199. self.exit_registration_mode
  200. )
  201. self._registration_timer.start()
  202. # Trigger event
  203. self._trigger_event("registration_mode_entered", {
  204. 'timeout_seconds': timeout_seconds
  205. })
  206. def exit_registration_mode(self) -> None:
  207. """Exit registration mode."""
  208. if not self._registration_mode:
  209. return
  210. pprint("Exiting registration mode")
  211. self._registration_mode = False
  212. if self._registration_timer:
  213. self._registration_timer.cancel()
  214. self._registration_timer = None
  215. # Trigger event
  216. self._trigger_event("registration_mode_exited", {})
  217. def is_registration_mode(self) -> bool:
  218. """Check if in registration mode."""
  219. return self._registration_mode
  220. def send_message(self, connection_id: str, command: TrixyCommand) -> bool:
  221. """
  222. Send a message to a specific client.
  223. Args:
  224. connection_id: Target connection ID
  225. command: Command to send
  226. Returns:
  227. bool: True if sent successfully
  228. """
  229. with self._connections_lock:
  230. connection = self._connections.get(connection_id)
  231. if not connection:
  232. return False
  233. try:
  234. # Create protocol message
  235. message = self.protocol.create_message(
  236. command.get_command_name(),
  237. command
  238. )
  239. # Serialize and send
  240. data = message.serialize()
  241. connection.socket.send(data)
  242. # Update statistics
  243. connection.bytes_sent += len(data)
  244. connection.commands_sent += 1
  245. pprint(f"Sent {command.get_command_name()} to {connection_id}")
  246. return True
  247. except Exception as e:
  248. pprint(f"Error sending message to {connection_id}: {e}")
  249. return False
  250. def broadcast_message(self, command: TrixyCommand, authenticated_only: bool = False) -> int:
  251. """
  252. Broadcast a message to all connected clients.
  253. Args:
  254. command: Command to broadcast
  255. authenticated_only: Only send to authenticated clients
  256. Returns:
  257. int: Number of clients that received the message
  258. """
  259. sent_count = 0
  260. with self._connections_lock:
  261. for connection in self._connections.values():
  262. if authenticated_only and not connection.authenticated:
  263. continue
  264. if self.send_message(connection.connection_id, command):
  265. sent_count += 1
  266. return sent_count
  267. def get_connection_info(self, connection_id: str) -> Optional[Dict[str, Any]]:
  268. """Get information about a connection."""
  269. with self._connections_lock:
  270. connection = self._connections.get(connection_id)
  271. return connection.get_connection_info() if connection else None
  272. def get_all_connections(self) -> Dict[str, Dict[str, Any]]:
  273. """Get information about all connections."""
  274. with self._connections_lock:
  275. return {
  276. cid: conn.get_connection_info()
  277. for cid, conn in self._connections.items()
  278. }
  279. def get_statistics(self) -> Dict[str, Any]:
  280. """Get server statistics."""
  281. with self._connections_lock:
  282. active_connections = len(self._connections)
  283. authenticated_connections = sum(
  284. 1 for conn in self._connections.values()
  285. if conn.authenticated
  286. )
  287. return {
  288. **self._stats,
  289. 'active_connections': active_connections,
  290. 'authenticated_connections': authenticated_connections,
  291. 'registration_mode': self._registration_mode,
  292. 'listen_port': self._listen_port,
  293. 'running': self._running
  294. }
  295. def _accept_connections(self) -> None:
  296. """Accept incoming connections (runs in separate thread)."""
  297. pprint("Started accepting connections")
  298. while self._running and self._server_socket:
  299. try:
  300. # Use select with timeout to allow clean shutdown
  301. ready, _, _ = select.select([self._server_socket], [], [], 1.0)
  302. if ready and self._running:
  303. client_socket, address = self._server_socket.accept()
  304. # Create connection
  305. connection_id = f"client_{address[0]}_{address[1]}_{uuid.uuid4().hex[:8]}"
  306. connection = ClientConnection(
  307. connection_id=connection_id,
  308. socket=client_socket,
  309. address=address
  310. )
  311. # Add to connections
  312. with self._connections_lock:
  313. self._connections[connection_id] = connection
  314. # Handle in thread pool
  315. self._thread_pool.submit(self._handle_client, connection)
  316. # Update statistics
  317. self._stats['connections_accepted'] += 1
  318. pprint(f"Accepted connection from {address}: {connection_id}")
  319. except Exception as e:
  320. if self._running:
  321. pprint(f"Error accepting connection: {e}")
  322. self._stats['errors'] += 1
  323. def _handle_client(self, connection: ClientConnection) -> None:
  324. """Handle a client connection (runs in thread pool)."""
  325. connection_id = connection.connection_id
  326. try:
  327. pprint(f"Handling client: {connection_id}")
  328. # Set socket timeout
  329. connection.socket.settimeout(30.0)
  330. while self._running:
  331. try:
  332. # Receive data
  333. data = connection.socket.recv(4096)
  334. if not data:
  335. break
  336. # Update connection activity
  337. connection.update_activity(len(data))
  338. # Process the data
  339. self._process_client_data(connection, data)
  340. except socket.timeout:
  341. # Check if connection timed out
  342. if connection.is_timeout():
  343. pprint(f"Connection {connection_id} timed out")
  344. break
  345. continue
  346. except Exception as e:
  347. pprint(f"Error handling client {connection_id}: {e}")
  348. self._stats['errors'] += 1
  349. break
  350. except Exception as e:
  351. pprint(f"Client handler error for {connection_id}: {e}")
  352. self._stats['errors'] += 1
  353. finally:
  354. self._close_connection(connection_id)
  355. def _process_client_data(self, connection: ClientConnection, data: bytes) -> None:
  356. """Process data received from a client."""
  357. try:
  358. # Check for hard-coded commands
  359. if self.protocol.is_hardcoded_command(data):
  360. command, args = self.protocol.parse_hardcoded_command(data)
  361. self._handle_hardcoded_command(connection, command, args)
  362. else:
  363. # Parse protocol message
  364. message = self.protocol.deserialize_message(data)
  365. self._handle_message(connection, message)
  366. # Update statistics
  367. connection.commands_received += 1
  368. self._stats['messages_processed'] += 1
  369. except Exception as e:
  370. pprint(f"Error processing data from {connection.connection_id}: {e}")
  371. self._stats['errors'] += 1
  372. def _handle_hardcoded_command(self, connection: ClientConnection, command: str, args: str) -> None:
  373. """Handle hard-coded commands."""
  374. try:
  375. if command == "TRXINOOP":
  376. # Heartbeat - just update activity (already done)
  377. pass
  378. elif command == "TRXIPING":
  379. # Respond with pong
  380. pong_response = f"TRXIPONG {args}".encode('utf-8')
  381. connection.socket.send(pong_response)
  382. connection.bytes_sent += len(pong_response)
  383. elif command == "TRXIPRNT":
  384. # Print command for debugging
  385. pprint(f"Client {connection.connection_id} print: {args}")
  386. elif command == "TRXYHELO":
  387. # Hello command for debugging
  388. pprint(f"Client {connection.connection_id} hello: {args}")
  389. except Exception as e:
  390. pprint(f"Error handling hard-coded command {command}: {e}")
  391. def _handle_message(self, connection: ClientConnection, message: TrixyMessage) -> None:
  392. """Handle a protocol message from a client."""
  393. command_name = message.class_name
  394. try:
  395. # Check for registered handler
  396. handler = self._message_handlers.get(command_name)
  397. if handler:
  398. handler(connection, message)
  399. else:
  400. pprint(f"No handler for command: {command_name}")
  401. except Exception as e:
  402. pprint(f"Error handling message {command_name}: {e}")
  403. def _setup_default_handlers(self) -> None:
  404. """Set up default message handlers."""
  405. self._message_handlers = {
  406. 'SatelliteHelloCommand': self._handle_satellite_hello,
  407. 'SatelliteRegistrationCommand': self._handle_satellite_registration,
  408. 'SatelliteConnectionCommand': self._handle_satellite_connection,
  409. }
  410. def _handle_satellite_hello(self, connection: ClientConnection, message: TrixyMessage) -> None:
  411. """Handle satellite hello command."""
  412. try:
  413. # Deserialize command data
  414. hello_cmd = SatelliteHelloCommand.from_dict(message.data.to_dict())
  415. pprint(f"Satellite hello from {hello_cmd.alias} ({hello_cmd.mac_address})")
  416. # Check if satellite is registered
  417. # This would check against registration files in a real implementation
  418. registered = True # Placeholder
  419. if registered:
  420. # Create positive response
  421. response = create_hello_response(
  422. command_id=hello_cmd.command_id,
  423. accepted=True,
  424. assigned_satellite_id=f"sat_{hello_cmd.mac_address.replace(':', '')}",
  425. registration_required=False
  426. )
  427. else:
  428. # Need registration
  429. response = create_hello_response(
  430. command_id=hello_cmd.command_id,
  431. accepted=False,
  432. registration_required=True,
  433. next_steps=["Enter registration mode on server", "Send registration command"]
  434. )
  435. # Send response
  436. self.send_message(connection.connection_id, response)
  437. except Exception as e:
  438. pprint(f"Error handling satellite hello: {e}")
  439. def _handle_satellite_registration(self, connection: ClientConnection, message: TrixyMessage) -> None:
  440. """Handle satellite registration command."""
  441. try:
  442. # Check if in registration mode
  443. if not self._registration_mode:
  444. response = create_registration_response(
  445. command_id=message.data.command_id,
  446. registration_accepted=False,
  447. rejection_reason="Server not in registration mode"
  448. )
  449. self.send_message(connection.connection_id, response)
  450. return
  451. # Deserialize command data
  452. reg_cmd = SatelliteRegistrationCommand.from_dict(message.data.to_dict())
  453. pprint(f"Satellite registration: {reg_cmd.alias} in {reg_cmd.room_id}")
  454. # Create satellite ID
  455. satellite_id = f"sat_{reg_cmd.mac_address.replace(':', '')}"
  456. # In a real implementation, this would:
  457. # 1. Create registration file
  458. # 2. Validate MAC address format
  459. # 3. Check for conflicts
  460. # Create positive response
  461. response = create_registration_response(
  462. command_id=reg_cmd.command_id,
  463. registration_accepted=True,
  464. satellite_id=satellite_id
  465. )
  466. # Update connection info
  467. connection.satellite_id = satellite_id
  468. connection.satellite_info = {
  469. 'room_id': reg_cmd.room_id,
  470. 'alias': reg_cmd.alias,
  471. 'mac_address': reg_cmd.mac_address,
  472. 'device_info': reg_cmd.device_info
  473. }
  474. # Send response
  475. self.send_message(connection.connection_id, response)
  476. # Trigger event
  477. self._trigger_event("satellite_registered", {
  478. 'satellite_id': satellite_id,
  479. 'connection_id': connection.connection_id,
  480. 'satellite_info': connection.satellite_info
  481. })
  482. except Exception as e:
  483. pprint(f"Error handling satellite registration: {e}")
  484. def _handle_satellite_connection(self, connection: ClientConnection, message: TrixyMessage) -> None:
  485. """Handle satellite connection command."""
  486. try:
  487. # Deserialize command data
  488. conn_cmd = SatelliteConnectionCommand.from_dict(message.data.to_dict())
  489. pprint(f"Satellite connection request: {conn_cmd.satellite_id}")
  490. # Validate satellite ID and MAC
  491. # In a real implementation, check registration files
  492. # Assign audio ports
  493. assigned_ports = {
  494. 'audio_input': 2102,
  495. 'audio_output': 2103,
  496. 'music': 2104
  497. }
  498. # Create positive response
  499. response = create_connection_response(
  500. command_id=conn_cmd.command_id,
  501. connection_accepted=True,
  502. assigned_audio_ports=assigned_ports,
  503. session_id=str(uuid.uuid4())
  504. )
  505. # Update connection state
  506. connection.authenticated = True
  507. connection.audio_streams = assigned_ports
  508. # Send response
  509. self.send_message(connection.connection_id, response)
  510. # Trigger event
  511. self._trigger_event("satellite_connected", {
  512. 'satellite_id': conn_cmd.satellite_id,
  513. 'connection_id': connection.connection_id,
  514. 'assigned_ports': assigned_ports
  515. })
  516. except Exception as e:
  517. pprint(f"Error handling satellite connection: {e}")
  518. def _close_connection(self, connection_id: str) -> None:
  519. """Close a client connection."""
  520. with self._connections_lock:
  521. connection = self._connections.pop(connection_id, None)
  522. if connection:
  523. try:
  524. connection.socket.close()
  525. except:
  526. pass
  527. pprint(f"Closed connection: {connection_id}")
  528. # Trigger event if it was an authenticated satellite
  529. if connection.authenticated and connection.satellite_id:
  530. self._trigger_event("satellite_disconnected", {
  531. 'satellite_id': connection.satellite_id,
  532. 'connection_id': connection_id
  533. })
  534. def _cleanup_connections(self) -> None:
  535. """Clean up timed out connections (runs in separate thread)."""
  536. while self._running:
  537. try:
  538. time.sleep(30.0) # Check every 30 seconds
  539. timed_out = []
  540. with self._connections_lock:
  541. for connection_id, connection in self._connections.items():
  542. if connection.is_timeout():
  543. timed_out.append(connection_id)
  544. # Close timed out connections
  545. for connection_id in timed_out:
  546. pprint(f"Closing timed out connection: {connection_id}")
  547. self._close_connection(connection_id)
  548. except Exception as e:
  549. if self._running:
  550. pprint(f"Error in cleanup thread: {e}")
  551. def _trigger_event(self, event_name: str, event_data: Dict[str, Any]) -> None:
  552. """Trigger an event through the application event system."""
  553. try:
  554. event_handler = self.application.get_event_handler()
  555. if event_handler:
  556. event_handler.trigger_event(event_name, event_data)
  557. except Exception as e:
  558. pprint(f"Error triggering event {event_name}: {e}")
  559. class TrixyServer:
  560. """
  561. High-level Trixy server implementation.
  562. This class provides a simple interface to the server functionality
  563. and integrates with the application container.
  564. """
  565. def __init__(self, application_container):
  566. """Initialize the Trixy server."""
  567. self.application = application_container
  568. self.server_handler = ServerSocketHandler(application_container)
  569. def start(self, port: int = 2101) -> None:
  570. """Start the server."""
  571. self.server_handler.start(port)
  572. def stop(self) -> None:
  573. """Stop the server."""
  574. self.server_handler.stop()
  575. def enter_registration_mode(self, timeout: float = 60.0) -> None:
  576. """Enter registration mode."""
  577. self.server_handler.enter_registration_mode(timeout)
  578. def exit_registration_mode(self) -> None:
  579. """Exit registration mode."""
  580. self.server_handler.exit_registration_mode()
  581. def get_status(self) -> Dict[str, Any]:
  582. """Get server status."""
  583. return self.server_handler.get_statistics()
  584. # Module exports
  585. __all__ = [
  586. "TrixyServer",
  587. "ServerSocketHandler",
  588. "ClientConnection",
  589. "ServerError",
  590. "PortInUseError",
  591. "ClientConnectionError",
  592. ]