Files
plba/src/app_runtime/control/http_runner.py
T

63 lines
2.3 KiB
Python

from __future__ import annotations
import asyncio
from threading import Thread
from fastapi import FastAPI
from uvicorn import Config, Server
class UvicornThreadRunner:
def __init__(self, host: str, port: int, timeout: int, *, thread_name: str = "plba-http-control") -> None:
self._host = host
self._port = port
self._timeout = timeout
self._thread_name = thread_name
self._server: Server | None = None
self._thread: Thread | None = None
self._error: BaseException | None = None
async def start(self, app: FastAPI) -> None:
if self._thread is not None and self._thread.is_alive():
return
self._error = None
config = Config(app=app, host=self._host, port=self._port, log_level="warning")
self._server = Server(config)
self._thread = Thread(target=self._serve, name=self._thread_name, daemon=True)
self._thread.start()
await self._wait_until_started()
async def stop(self) -> None:
if self._server is None or self._thread is None:
return
self._server.should_exit = True
await asyncio.to_thread(self._thread.join, self._timeout)
self._server = None
self._thread = None
@property
def port(self) -> int:
if self._server is None or not getattr(self._server, "servers", None):
return self._port
socket = self._server.servers[0].sockets[0]
return int(socket.getsockname()[1])
async def _wait_until_started(self) -> None:
if self._server is None:
raise RuntimeError("Server is not initialized")
deadline = asyncio.get_running_loop().time() + max(float(self._timeout), 1.0)
while not self._server.started:
if self._error is not None:
raise RuntimeError("HTTP control server failed to start") from self._error
if asyncio.get_running_loop().time() >= deadline:
raise TimeoutError("HTTP control server startup timed out")
await asyncio.sleep(0.05)
def _serve(self) -> None:
if self._server is None:
return
try:
asyncio.run(self._server.serve())
except BaseException as exc: # noqa: BLE001
self._error = exc