100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""
|
|
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") |