""" Unix-domain socket server that receives opportunity signals from oe_em. Each JSON line on the socket is parsed and dispatched as an asyncio task to avoid blocking the reader. """ import asyncio import json import time from pathlib import Path import structlog logger = structlog.get_logger().bind(component="signal_socket_server") class SignalSocketServer: """ Accepts JSON-serialized signals over a Unix domain socket. Every valid "signal" message is wrapped in create_task so processing is concurrent and a slow handler never blocks new connections. """ def __init__(self, socket_path: Path, on_signal) -> None: self._socket_path = socket_path self._on_signal = on_signal self._log = logger self._server: asyncio.Server | None = None self._running = False async def start(self) -> None: """Remove any stale socket file and start accepting connections.""" if self._socket_path.exists(): self._socket_path.unlink() self._running = True self._server = await asyncio.start_unix_server( self._accept_client, path=str(self._socket_path), ) self._log.info("signal_socket_server_started", path=str(self._socket_path)) async with self._server: await self._server.serve_forever() async def stop(self) -> None: """Stop the server and remove the socket file.""" self._running = False if self._server: self._server.close() await self._server.wait_closed() if self._socket_path.exists(): self._socket_path.unlink() self._log.info("signal_socket_server_stopped") async def _accept_client( self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter ) -> None: """ Handle one client connection: read lines, parse JSON, dispatch signals. The client is assumed to be oe_em's SignalSocketClient. Any line that is not JSON or is not type="signal" is silently ignored. """ self._log.info("client_connected", addr=writer.get_extra_info("peername")) try: while self._running: try: line = await reader.readline() except (ConnectionResetError, BrokenPipeError, asyncio.CancelledError): break except Exception: break if not line: break arrived_ms = int(time.time() * 1000) try: data = json.loads(line.decode()) except (json.JSONDecodeError, UnicodeDecodeError) as e: self._log.warning("invalid_json", line=line[:50], error=str(e)) continue if data.get("type") == "signal": data["_receiver_ts_ms"] = arrived_ms asyncio.create_task(self._on_signal(data)) else: self._log.debug("ignored_non_signal", type=data.get("type")) except asyncio.CancelledError: pass except Exception as e: self._log.warning("client_error", error=str(e)) finally: writer.close() try: await asyncio.wait_for(writer.wait_closed(), timeout=1.0) except Exception: pass self._log.info("client_disconnected")