"""Run the web UI's FastAPI app on a background thread. The radar app already owns the hardware and the Qt event loop, so the web server lives in a daemon thread inside that process (uvicorn brings its own asyncio loop for the thread). Control requests hop back to the Qt main thread via the bridge's queued signals — the web thread never touches Qt directly. """ from __future__ import annotations import logging import threading import uvicorn from python_app.webui.app import create_app from python_app.webui.controller import WebController logger = logging.getLogger(__name__) class WebUiServer: """Owns a uvicorn server bound to a controller, run on a daemon thread.""" def __init__(self, controller: WebController, *, host: str = "0.0.0.0", port: int = 8080) -> None: """Build the server for ``controller`` (not started until :meth:`start`).""" config = uvicorn.Config(create_app(controller), host=host, port=port, log_level="warning") self._server = uvicorn.Server(config) self._thread = threading.Thread(target=self._serve, name="radar-webui", daemon=True) def start(self) -> None: """Start serving on the background thread.""" self._thread.start() def is_alive(self) -> bool: """Return whether the server thread is still running.""" return self._thread.is_alive() def _serve(self) -> None: """Run uvicorn, surfacing a startup/runtime failure instead of dying silently. The bind happens on this thread after ``start()`` has already returned, so a failure (e.g. the port is taken) would otherwise be invisible. """ try: self._server.run() except Exception: # noqa: BLE001 - log, never crash the host process logger.exception("Web UI server thread exited with an error") def stop(self) -> None: """Ask uvicorn to exit and wait briefly for the thread to unwind.""" self._server.should_exit = True self._thread.join(timeout=5.0)