service.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. # -*- coding: utf-8 -*-
  2. """
  3. NetworkService - TCP-Server für Trixy.
  4. Implementiert die Server-seitige Netzwerk-Transport-Schicht.
  5. Horcht auf 4 Ports:
  6. - 2101: Command Socket (Befehle, verschlüsselt)
  7. - 2102: Audio Input (Satellite → Server)
  8. - 2103: Audio Output (Server → Satellite)
  9. - 2104: Music Stream (Server → Satellite)
  10. """
  11. from __future__ import annotations
  12. import asyncio
  13. import struct
  14. from typing import TYPE_CHECKING, Callable, Awaitable
  15. from trixy_core.service.iservice import IService
  16. from trixy_core.service.enums import ServicePriority, ServiceGroup
  17. from trixy_core.network.protocol import (
  18. TrixyProtocol,
  19. ProtocolFlags,
  20. ProtocolMessage,
  21. MAGIC,
  22. MAGIC_LENGTH,
  23. COMMAND_HELLO,
  24. COMMAND_PING,
  25. COMMAND_PONG,
  26. )
  27. from trixy_core.network.encryption import TrixyEncryption
  28. from trixy_core.network.keepalive import KeepAliveManager, KeepAliveConfig
  29. from trixy_core.network.cmd import (
  30. SatelliteConnect,
  31. SatelliteDisconnect,
  32. SatelliteAccepted,
  33. SatelliteDenied,
  34. )
  35. from trixy_core.satellite.satellite import Satellite, ConnectionState
  36. from trixy_core.events.event_data.basic import SatelliteConnected, SatelliteDisconnected
  37. from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
  38. if TYPE_CHECKING:
  39. from trixy_core.server import ServerApplication
  40. class ClientHandler:
  41. """
  42. Handler für eine einzelne Client-Verbindung.
  43. Verwaltet den Lebenszyklus einer Verbindung vom Handshake
  44. bis zur Trennung.
  45. """
  46. def __init__(
  47. self,
  48. reader: asyncio.StreamReader,
  49. writer: asyncio.StreamWriter,
  50. protocol: TrixyProtocol,
  51. service: "NetworkService",
  52. ) -> None:
  53. self._reader = reader
  54. self._writer = writer
  55. self._protocol = protocol
  56. self._service = service
  57. self._satellite: Satellite | None = None
  58. self._authenticated = False
  59. self._running = True
  60. self._peer_address = writer.get_extra_info("peername")
  61. @property
  62. def satellite(self) -> Satellite | None:
  63. """Der zugehörige Satellite."""
  64. return self._satellite
  65. @property
  66. def is_authenticated(self) -> bool:
  67. """Ist der Client authentifiziert?"""
  68. return self._authenticated
  69. @property
  70. def peer_address(self) -> tuple:
  71. """Adresse des Clients."""
  72. return self._peer_address
  73. async def run(self) -> None:
  74. """Hauptschleife für die Verbindung."""
  75. try:
  76. # Handshake durchführen
  77. if not await self._perform_handshake():
  78. return
  79. # Nachrichtenloop
  80. await self._message_loop()
  81. except asyncio.CancelledError:
  82. pdebug(f"Verbindung abgebrochen: {self._peer_address}")
  83. except ConnectionResetError:
  84. pdebug(f"Verbindung zurückgesetzt: {self._peer_address}")
  85. except Exception as e:
  86. perror(f"Verbindungsfehler {self._peer_address}: {e}")
  87. finally:
  88. await self._cleanup()
  89. async def _perform_handshake(self) -> bool:
  90. """
  91. Führt den Handshake mit dem Client durch.
  92. Ablauf:
  93. 1. Empfange HELLO
  94. 2. Empfange SatelliteConnect
  95. 3. Prüfe Registrierung
  96. 4. Sende SatelliteAccepted/Denied
  97. Returns:
  98. True bei erfolgreichem Handshake
  99. """
  100. pdebug(f"Handshake gestartet: {self._peer_address}")
  101. # Warte auf HELLO (max 10 Sekunden)
  102. try:
  103. hello = await asyncio.wait_for(
  104. self._read_raw(8),
  105. timeout=10.0
  106. )
  107. except asyncio.TimeoutError:
  108. pwarn(f"Timeout beim Handshake: {self._peer_address}")
  109. return False
  110. if hello != COMMAND_HELLO:
  111. pwarn(f"Ungültiges HELLO von {self._peer_address}: {hello}")
  112. return False
  113. pdebug(f"HELLO empfangen von {self._peer_address}")
  114. # Warte auf SatelliteConnect
  115. try:
  116. message = await asyncio.wait_for(
  117. self._read_message(),
  118. timeout=10.0
  119. )
  120. except asyncio.TimeoutError:
  121. pwarn(f"Timeout beim Empfang von SatelliteConnect: {self._peer_address}")
  122. return False
  123. if message is None or message.class_name != "SatelliteConnect":
  124. pwarn(f"Erwartete SatelliteConnect, erhielt: {message.class_name if message else 'None'}")
  125. return False
  126. connect_data = message.data
  127. if isinstance(connect_data, dict):
  128. connect_msg = SatelliteConnect(**{
  129. k: v for k, v in connect_data.items()
  130. if k in SatelliteConnect.__dataclass_fields__
  131. })
  132. elif isinstance(connect_data, SatelliteConnect):
  133. connect_msg = connect_data
  134. else:
  135. pwarn(f"Ungültige SatelliteConnect-Daten: {type(connect_data)}")
  136. return False
  137. pdebug(f"SatelliteConnect empfangen: {connect_msg.alias} ({connect_msg.mac_address})")
  138. # Prüfe Registrierung
  139. app = self._service.application
  140. is_registered = app.registration.is_registered(connect_msg.mac_address)
  141. if not is_registered:
  142. # Prüfe ob Registrierung erlaubt (Permanent/Zeitfenster/Datei-Trigger)
  143. if app.registration.should_auto_register():
  144. # Starte Registrierung
  145. app.registration.begin_registration(
  146. connect_msg.mac_address,
  147. connect_msg.room,
  148. connect_msg.alias
  149. )
  150. # Vervollständige Registrierung automatisch
  151. satellite = app.registration.complete_registration(connect_msg.mac_address)
  152. if satellite:
  153. app.satellites.add(satellite)
  154. is_registered = True
  155. if not is_registered:
  156. pwarn(f"Unregistrierter Satellite: {connect_msg.mac_address}")
  157. denied = SatelliteDenied(
  158. reason="Satellite nicht registriert",
  159. retry_allowed=True,
  160. retry_after_seconds=60
  161. )
  162. await self._send_message(denied)
  163. return False
  164. # Hole oder erstelle Satellite
  165. satellite = app.satellites.get_by_mac(connect_msg.mac_address)
  166. if satellite is None:
  167. satellite = app.registration.load_registered(connect_msg.mac_address)
  168. if satellite:
  169. app.satellites.add(satellite)
  170. if satellite is None:
  171. perror(f"Satellite konnte nicht geladen werden: {connect_msg.mac_address}")
  172. denied = SatelliteDenied(
  173. reason="Interner Fehler",
  174. retry_allowed=True,
  175. retry_after_seconds=5
  176. )
  177. await self._send_message(denied)
  178. return False
  179. # Aktualisiere Satellite-Informationen
  180. satellite.ip_address = self._peer_address[0] if self._peer_address else ""
  181. satellite.alias = connect_msg.alias or satellite.alias
  182. satellite.room_id = connect_msg.room or satellite.room_id
  183. satellite.version = connect_msg.version
  184. # Metadata mit System-Infos befuellen
  185. if connect_msg.hostname:
  186. satellite.metadata["hostname"] = connect_msg.hostname
  187. if connect_msg.os_name:
  188. satellite.metadata["os_name"] = connect_msg.os_name
  189. if connect_msg.os_version:
  190. satellite.metadata["os_version"] = connect_msg.os_version
  191. # Session-Key generieren
  192. session_key = TrixyEncryption.generate_key()
  193. # Sockets zuweisen
  194. satellite.sockets.command = self._writer
  195. # Als verbunden markieren
  196. satellite.set_connected()
  197. self._satellite = satellite
  198. self._authenticated = True
  199. # Registriere bei KeepAlive
  200. await self._service.keepalive.register(
  201. satellite.id,
  202. lambda: self._send_ping(),
  203. auto_start=True
  204. )
  205. # Sende Bestätigung mit Audio-Format-Info
  206. network_config = app.server_config.network
  207. audio_config = app.server_config.audio
  208. # TTS-Format vom aktiven TTS-Provider holen
  209. tts_sample_rate = 22050 # Default
  210. tts_channels = 1
  211. try:
  212. extensions = getattr(app, "extensions", None)
  213. if extensions:
  214. tts_point = extensions.get_point("conversation.tts")
  215. if tts_point:
  216. tts_provider = tts_point.get_first_instance()
  217. if tts_provider and hasattr(tts_provider, "config"):
  218. rate = tts_provider.config.sample_rate
  219. chans = tts_provider.config.channels
  220. # Nur verwenden wenn es echte int-Werte sind
  221. if isinstance(rate, int) and isinstance(chans, int):
  222. tts_sample_rate = rate
  223. tts_channels = chans
  224. except Exception:
  225. pass # Bei Fehlern Default-Werte verwenden
  226. # Musik-Format aus Server-Config
  227. music_sample_rate = audio_config.music_sample_rate
  228. music_channels = audio_config.music_channels
  229. music_bit_depth = getattr(audio_config, 'music_bit_depth', 16)
  230. accepted = SatelliteAccepted(
  231. satellite_id=satellite.id,
  232. session_key=session_key,
  233. audio_input_port=network_config.audio_input_port,
  234. audio_output_port=network_config.audio_output_port,
  235. music_port=network_config.music_port,
  236. # TTS-Format vom aktiven Plugin
  237. tts_sample_rate=tts_sample_rate,
  238. tts_channels=tts_channels,
  239. tts_sample_width=2, # 16-bit
  240. # Musik-Format aus Config (konfigurierbar für Audio-Device-Kompatibilität)
  241. music_sample_rate=music_sample_rate,
  242. music_channels=music_channels,
  243. music_sample_width=music_bit_depth // 8, # Bytes
  244. )
  245. await self._send_message(accepted)
  246. pdebug(f"Musik-Format an Client: {music_sample_rate}Hz, {music_channels}ch, {music_bit_depth}-bit")
  247. pinfo(f"Satellite verbunden: {satellite.alias} ({satellite.room_id})")
  248. # Event auslösen
  249. await app.events.trigger("satellite_connected", SatelliteConnected(
  250. satellite_id=satellite.id,
  251. mac_address=satellite.mac_address,
  252. room=satellite.room_id,
  253. alias=satellite.alias,
  254. ip_address=satellite.ip_address,
  255. source="network_service"
  256. ))
  257. return True
  258. async def _message_loop(self) -> None:
  259. """Verarbeitet eingehende Nachrichten."""
  260. while self._running:
  261. message = await self._read_message()
  262. if message is None:
  263. break
  264. await self._handle_message(message)
  265. async def _handle_message(self, message: ProtocolMessage) -> None:
  266. """
  267. Verarbeitet eine empfangene Nachricht.
  268. Args:
  269. message: Die empfangene Nachricht
  270. """
  271. class_name = message.class_name
  272. # Hard-coded Befehle
  273. if class_name == "TRXIPONG":
  274. # Pong empfangen - KeepAlive aktualisieren
  275. if self._satellite:
  276. self._satellite.update_heartbeat()
  277. self._service.keepalive.handle_pong(self._satellite.id)
  278. return
  279. if class_name == "TRXIPING":
  280. # Ping empfangen - Pong senden
  281. await self._send_pong()
  282. return
  283. # SatelliteDisconnect
  284. if class_name == "SatelliteDisconnect":
  285. data = message.data
  286. reason = data.get("reason", "Client-Trennung") if isinstance(data, dict) else "Client-Trennung"
  287. pinfo(f"Satellite trennt Verbindung: {reason}")
  288. self._running = False
  289. return
  290. # Pending-Response pruefen (Config-Proxy)
  291. if self._satellite:
  292. msg_id = ""
  293. data = message.data
  294. if hasattr(data, "message_id"):
  295. msg_id = data.message_id
  296. elif isinstance(data, dict):
  297. msg_id = data.get("message_id", "")
  298. if msg_id and self._satellite.resolve_pending(msg_id, message):
  299. return
  300. # Andere Nachrichten an MessageHandler weiterleiten
  301. if self._satellite:
  302. await self._service.dispatch_message(self._satellite, message)
  303. async def _read_raw(self, size: int) -> bytes:
  304. """Liest rohe Bytes vom Socket."""
  305. data = await self._reader.read(size)
  306. if not data:
  307. raise ConnectionResetError("Verbindung geschlossen")
  308. return data
  309. async def _read_message(self) -> ProtocolMessage | None:
  310. """
  311. Liest eine vollständige Protokoll-Nachricht.
  312. Returns:
  313. Die gelesene Nachricht oder None bei Verbindungsende
  314. """
  315. try:
  316. # Lese Header (Magic + Version + Timestamp + Flags + Checksum + ClassNameLength)
  317. header_size = MAGIC_LENGTH + 2 + 8 + 4 + 16 + 2 # 36 bytes
  318. # Prüfe zuerst auf Hard-coded Befehle (8 bytes)
  319. # Verwende readexactly um sicherzustellen, dass wir alle 8 Bytes bekommen
  320. peek_data = await self._reader.readexactly(8)
  321. # Prüfe auf Hard-coded Befehl
  322. if self._protocol.is_hard_command(peek_data):
  323. return self._protocol.deserialize(peek_data)
  324. # Kein Hard-coded Befehl - lese Rest des Headers
  325. remaining_header = await self._reader.readexactly(header_size - 8)
  326. header = peek_data + remaining_header
  327. # Prüfe Magic
  328. if header[:MAGIC_LENGTH] != MAGIC:
  329. perror(f"Ungültige Magic: {header[:MAGIC_LENGTH]}")
  330. return None
  331. # Parse Header um Klassennamen-Länge und Daten-Länge zu bekommen
  332. # Offset nach Magic + Version + Timestamp + Flags + Checksum
  333. class_name_length = struct.unpack(">H", header[34:36])[0]
  334. # Lese Klassenname
  335. class_name = await self._reader.readexactly(class_name_length)
  336. # Lese Datenlänge (4 bytes)
  337. data_length_bytes = await self._reader.readexactly(4)
  338. data_length = struct.unpack(">I", data_length_bytes)[0]
  339. # Lese Daten
  340. data = await self._reader.readexactly(data_length) if data_length > 0 else b""
  341. # Baue vollständige Nachricht zusammen
  342. full_message = header + class_name + data_length_bytes + data
  343. return self._protocol.deserialize(full_message)
  344. except (ConnectionResetError, asyncio.IncompleteReadError):
  345. return None
  346. except Exception as e:
  347. perror(f"Fehler beim Lesen der Nachricht: {e}")
  348. return None
  349. async def _send_message(self, message: object, flags: ProtocolFlags = ProtocolFlags.PICKLE) -> bool:
  350. """
  351. Sendet eine Nachricht zum Client.
  352. Args:
  353. message: Die zu sendende Nachricht
  354. flags: Protokoll-Flags
  355. Returns:
  356. True bei Erfolg
  357. """
  358. try:
  359. data = self._protocol.serialize(message, flags)
  360. self._writer.write(data)
  361. await self._writer.drain()
  362. return True
  363. except Exception as e:
  364. perror(f"Fehler beim Senden: {e}")
  365. return False
  366. async def _send_ping(self) -> None:
  367. """Sendet einen Ping."""
  368. try:
  369. self._writer.write(COMMAND_PING)
  370. await self._writer.drain()
  371. except Exception as e:
  372. pdebug(f"Ping-Fehler: {e}")
  373. async def _send_pong(self) -> None:
  374. """Sendet einen Pong."""
  375. try:
  376. self._writer.write(COMMAND_PONG)
  377. await self._writer.drain()
  378. except Exception as e:
  379. pdebug(f"Pong-Fehler: {e}")
  380. async def _cleanup(self, reason: str = "Verbindung getrennt") -> None:
  381. """Räumt die Verbindung auf."""
  382. self._running = False
  383. if self._satellite:
  384. # KeepAlive entfernen
  385. await self._service.keepalive.unregister(self._satellite.id)
  386. # Event auslösen
  387. await self._service.application.events.trigger("satellite_disconnected", SatelliteDisconnected(
  388. satellite_id=self._satellite.id,
  389. room=self._satellite.room_id,
  390. alias=self._satellite.alias,
  391. reason=reason,
  392. source="network_service"
  393. ))
  394. # Satellite als getrennt markieren
  395. self._satellite.state = ConnectionState.DISCONNECTED
  396. self._satellite.sockets.command = None
  397. pinfo(f"Satellite getrennt: {self._satellite.alias}")
  398. self._satellite = None
  399. # Socket schließen
  400. try:
  401. self._writer.close()
  402. await self._writer.wait_closed()
  403. except Exception:
  404. pass
  405. class AudioStreamHandler:
  406. """
  407. Handler für Audio-Stream-Verbindungen.
  408. Verwaltet Audio-Input (Satellite → Server) und
  409. Audio-Output (Server → Satellite) Streams.
  410. """
  411. def __init__(
  412. self,
  413. reader: asyncio.StreamReader,
  414. writer: asyncio.StreamWriter,
  415. service: "NetworkService",
  416. stream_type: str,
  417. ) -> None:
  418. self._reader = reader
  419. self._writer = writer
  420. self._service = service
  421. self._stream_type = stream_type
  422. self._satellite: Satellite | None = None
  423. self._running = True
  424. self._peer_address = writer.get_extra_info("peername")
  425. async def run(self) -> None:
  426. """Hauptschleife für Audio-Stream."""
  427. try:
  428. # Warte auf Satellite-ID
  429. satellite_id_bytes = await asyncio.wait_for(
  430. self._reader.read(36), # UUID-Länge
  431. timeout=10.0
  432. )
  433. if not satellite_id_bytes:
  434. return
  435. satellite_id = satellite_id_bytes.decode("utf-8").strip()
  436. satellite = self._service.application.satellites.get(satellite_id)
  437. if not satellite or not satellite.is_connected:
  438. pwarn(f"Audio-Stream für unbekannten Satellite: {satellite_id}")
  439. return
  440. self._satellite = satellite
  441. pdebug(f"{self._stream_type} verbunden: {satellite.alias}")
  442. # Socket zuweisen
  443. if self._stream_type == "audio_in":
  444. satellite.sockets.audio_in = self._writer
  445. await self._handle_audio_input()
  446. elif self._stream_type == "audio_out":
  447. satellite.sockets.audio_out = self._writer
  448. await self._handle_audio_output()
  449. elif self._stream_type == "music":
  450. satellite.sockets.music_out = self._writer
  451. await self._handle_music_output()
  452. except asyncio.TimeoutError:
  453. pdebug(f"Audio-Stream Timeout: {self._peer_address}")
  454. except Exception as e:
  455. perror(f"Audio-Stream Fehler: {e}")
  456. finally:
  457. await self._cleanup()
  458. async def _handle_audio_input(self) -> None:
  459. """Empfängt Audio vom Satellite."""
  460. while self._running and self._satellite:
  461. try:
  462. # Lese Audio-Chunks (4096 bytes = 128ms bei 16KHz mono)
  463. chunk = await self._reader.read(4096)
  464. if not chunk:
  465. break
  466. # Event für Audio-Verarbeitung auslösen
  467. await self._service.application.events.emit(
  468. "audio_input_received",
  469. {
  470. "satellite_id": self._satellite.id,
  471. "audio_data": chunk,
  472. "sample_rate": 16000,
  473. "channels": 1,
  474. }
  475. )
  476. except Exception as e:
  477. pdebug(f"Audio-Input Fehler: {e}")
  478. break
  479. async def _handle_audio_output(self) -> None:
  480. """Verwaltet Audio-Output zum Satellite (wartet auf Events)."""
  481. # Audio-Output wird über Events gesteuert
  482. # Hier nur Verbindung aufrecht halten
  483. while self._running:
  484. await asyncio.sleep(1)
  485. async def _handle_music_output(self) -> None:
  486. """Verwaltet Music-Output zum Satellite (wartet auf Events)."""
  487. # Event auslösen, dass Music-Socket verbunden ist
  488. if self._satellite:
  489. from trixy_core.events.event_data.basic import MusicSocketConnected
  490. await self._service.application.events.trigger(
  491. "music_socket_connected",
  492. MusicSocketConnected(
  493. satellite_id=self._satellite.id,
  494. alias=self._satellite.alias,
  495. room=self._satellite.room_id,
  496. )
  497. )
  498. pdebug(f"Music-Socket verbunden: {self._satellite.alias}")
  499. # Music-Output wird über Events gesteuert
  500. while self._running:
  501. await asyncio.sleep(1)
  502. async def _cleanup(self) -> None:
  503. """Räumt den Stream auf."""
  504. self._running = False
  505. if self._satellite:
  506. if self._stream_type == "audio_in":
  507. self._satellite.sockets.audio_in = None
  508. elif self._stream_type == "audio_out":
  509. self._satellite.sockets.audio_out = None
  510. elif self._stream_type == "music":
  511. self._satellite.sockets.music_out = None
  512. try:
  513. self._writer.close()
  514. await self._writer.wait_closed()
  515. except Exception:
  516. pass
  517. class NetworkService(IService):
  518. """
  519. TCP-Server für Trixy-Kommunikation.
  520. Horcht auf 4 Ports und verwaltet alle Verbindungen:
  521. - Port 2101: Command Socket (Befehle, verschlüsselt)
  522. - Port 2102: Audio Input (Satellite → Server)
  523. - Port 2103: Audio Output (Server → Satellite)
  524. - Port 2104: Music Stream (Server → Satellite)
  525. """
  526. PRIORITY = ServicePriority.NETWORK
  527. GROUP = ServiceGroup.NETWORK
  528. NAME = "network"
  529. DEPENDENCIES: list[str] = []
  530. def __init__(self, application: "ServerApplication") -> None:
  531. super().__init__(application)
  532. self._servers: dict[str, asyncio.Server] = {}
  533. self._handlers: dict[str, set[ClientHandler | AudioStreamHandler]] = {
  534. "command": set(),
  535. "audio_in": set(),
  536. "audio_out": set(),
  537. "music": set(),
  538. }
  539. self._protocol = TrixyProtocol()
  540. self._encryption: TrixyEncryption | None = None
  541. self._keepalive = KeepAliveManager(
  542. default_config=KeepAliveConfig(
  543. ping_interval=30.0,
  544. pong_timeout=15.0, # Erhöht von 10s
  545. max_missed_pongs=5, # Erhöht von 3 (= 5*30s = 2.5min Toleranz)
  546. )
  547. )
  548. self._message_handlers: list[Callable[[Satellite, ProtocolMessage], Awaitable[None]]] = []
  549. @property
  550. def application(self) -> "ServerApplication":
  551. """Server-Anwendung."""
  552. return self._application # type: ignore
  553. @property
  554. def keepalive(self) -> KeepAliveManager:
  555. """KeepAlive-Manager."""
  556. return self._keepalive
  557. @property
  558. def protocol(self) -> TrixyProtocol:
  559. """Protokoll-Handler."""
  560. return self._protocol
  561. def register_message_handler(
  562. self,
  563. handler: Callable[[Satellite, ProtocolMessage], Awaitable[None]]
  564. ) -> None:
  565. """
  566. Registriert einen Handler für eingehende Nachrichten.
  567. Args:
  568. handler: Async-Funktion die (satellite, message) verarbeitet
  569. """
  570. self._message_handlers.append(handler)
  571. async def dispatch_message(self, satellite: Satellite, message: ProtocolMessage) -> None:
  572. """
  573. Verteilt eine Nachricht an alle registrierten Handler.
  574. Args:
  575. satellite: Der sendende Satellite
  576. message: Die empfangene Nachricht
  577. """
  578. for handler in self._message_handlers:
  579. try:
  580. await handler(satellite, message)
  581. except Exception as e:
  582. perror(f"Message-Handler Fehler: {e}")
  583. async def send_to_satellite(
  584. self,
  585. satellite_id: str,
  586. message: object,
  587. flags: ProtocolFlags = ProtocolFlags.PICKLE
  588. ) -> bool:
  589. """
  590. Sendet eine Nachricht an einen Satellite.
  591. Args:
  592. satellite_id: Ziel-Satellite-ID
  593. message: Die zu sendende Nachricht
  594. flags: Protokoll-Flags
  595. Returns:
  596. True bei Erfolg
  597. """
  598. satellite = self.application.satellites.get(satellite_id)
  599. if not satellite or not satellite.sockets.command:
  600. return False
  601. try:
  602. writer = satellite.sockets.command
  603. data = self._protocol.serialize(message, flags)
  604. writer.write(data)
  605. await writer.drain()
  606. return True
  607. except Exception as e:
  608. perror(f"Fehler beim Senden an {satellite_id}: {e}")
  609. return False
  610. async def broadcast(
  611. self,
  612. message: object,
  613. room: str | None = None,
  614. flags: ProtocolFlags = ProtocolFlags.PICKLE
  615. ) -> int:
  616. """
  617. Sendet eine Nachricht an alle (oder gefilterte) Satellites.
  618. Args:
  619. message: Die zu sendende Nachricht
  620. room: Optional: Nur an Satellites in diesem Raum
  621. flags: Protokoll-Flags
  622. Returns:
  623. Anzahl erfolgreicher Sendungen
  624. """
  625. satellites = self.application.satellites.get_by_room(room) if room else self.application.satellites.get_connected()
  626. count = 0
  627. for satellite in satellites:
  628. if await self.send_to_satellite(satellite.id, message, flags):
  629. count += 1
  630. return count
  631. async def start(self) -> None:
  632. """Startet alle TCP-Server."""
  633. config = self.application.server_config.network
  634. # Verschlüsselung laden
  635. if self.application.server_config.security.encryption_enabled:
  636. key_path = self.application.server_config.security.encryption_key_path
  637. try:
  638. self._encryption = TrixyEncryption.load_or_create(key_path)
  639. self._protocol.set_encryption(self._encryption)
  640. pdebug("Verschlüsselung aktiviert")
  641. except Exception as e:
  642. pwarn(f"Verschlüsselung konnte nicht geladen werden: {e}")
  643. # Command-Server starten (Port 2101)
  644. self._servers["command"] = await asyncio.start_server(
  645. self._handle_command_connection,
  646. config.bind_address,
  647. config.command_port,
  648. )
  649. pdebug(f"Command-Server gestartet auf {config.bind_address}:{config.command_port}")
  650. # Audio-Input-Server starten (Port 2102)
  651. self._servers["audio_in"] = await asyncio.start_server(
  652. lambda r, w: self._handle_audio_connection(r, w, "audio_in"),
  653. config.bind_address,
  654. config.audio_input_port,
  655. )
  656. pdebug(f"Audio-Input-Server gestartet auf {config.bind_address}:{config.audio_input_port}")
  657. # Audio-Output-Server starten (Port 2103)
  658. self._servers["audio_out"] = await asyncio.start_server(
  659. lambda r, w: self._handle_audio_connection(r, w, "audio_out"),
  660. config.bind_address,
  661. config.audio_output_port,
  662. )
  663. pdebug(f"Audio-Output-Server gestartet auf {config.bind_address}:{config.audio_output_port}")
  664. # Music-Server starten (Port 2104)
  665. self._servers["music"] = await asyncio.start_server(
  666. lambda r, w: self._handle_audio_connection(r, w, "music"),
  667. config.bind_address,
  668. config.music_port,
  669. )
  670. pdebug(f"Music-Server gestartet auf {config.bind_address}:{config.music_port}")
  671. # KeepAlive-Disconnect-Handler registrieren
  672. self._keepalive.on_disconnect(self._on_keepalive_disconnect)
  673. pinfo(f"NetworkService gestartet auf Ports {config.command_port}-{config.music_port}")
  674. async def stop(self) -> None:
  675. """Stoppt alle Server und Verbindungen."""
  676. pinfo("NetworkService wird gestoppt...")
  677. # KeepAlive stoppen
  678. await self._keepalive.stop_all()
  679. # Alle Handler beenden
  680. for handler_type, handlers in self._handlers.items():
  681. for handler in list(handlers):
  682. if hasattr(handler, "_running"):
  683. handler._running = False
  684. # Server schließen
  685. for name, server in self._servers.items():
  686. server.close()
  687. await server.wait_closed()
  688. pdebug(f"{name}-Server geschlossen")
  689. self._servers.clear()
  690. pinfo("NetworkService gestoppt")
  691. async def _handle_command_connection(
  692. self,
  693. reader: asyncio.StreamReader,
  694. writer: asyncio.StreamWriter
  695. ) -> None:
  696. """Verarbeitet eine neue Command-Verbindung."""
  697. addr = writer.get_extra_info("peername")
  698. pdebug(f"Neue Command-Verbindung von {addr}")
  699. handler = ClientHandler(reader, writer, self._protocol, self)
  700. self._handlers["command"].add(handler)
  701. try:
  702. await handler.run()
  703. finally:
  704. self._handlers["command"].discard(handler)
  705. async def _handle_audio_connection(
  706. self,
  707. reader: asyncio.StreamReader,
  708. writer: asyncio.StreamWriter,
  709. stream_type: str
  710. ) -> None:
  711. """Verarbeitet eine neue Audio-Verbindung."""
  712. addr = writer.get_extra_info("peername")
  713. pdebug(f"Neue {stream_type}-Verbindung von {addr}")
  714. handler = AudioStreamHandler(reader, writer, self, stream_type)
  715. self._handlers[stream_type].add(handler)
  716. try:
  717. await handler.run()
  718. finally:
  719. self._handlers[stream_type].discard(handler)
  720. async def _on_keepalive_disconnect(self, connection_id: str) -> None:
  721. """Handler für KeepAlive-Disconnect."""
  722. satellite = self.application.satellites.get(connection_id)
  723. if satellite:
  724. pwarn(f"KeepAlive-Timeout für {satellite.alias}")
  725. await satellite.disconnect("KeepAlive-Timeout")
  726. async def health_check(self) -> bool:
  727. """Prüft den Gesundheitszustand des Services."""
  728. # Prüfe ob Server existieren und laufen
  729. if not self._servers:
  730. return False
  731. for name, server in self._servers.items():
  732. if not server.is_serving():
  733. return False
  734. return True