63 lines
2.3 KiB
Python
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
|