|
1 | 1 | import asyncio |
2 | 2 | import json |
3 | 3 | import logging |
| 4 | +import sys |
4 | 5 | import uuid |
5 | 6 | from threading import Event |
6 | 7 | from typing import Any, Dict, Optional, Tuple, Type, Union, cast |
7 | 8 |
|
8 | | -import uvicorn |
9 | 9 | from fastapi import APIRouter, FastAPI, Request, WebSocket |
10 | 10 | from fastapi.middleware.cors import CORSMiddleware |
11 | 11 | from fastapi.responses import RedirectResponse |
12 | 12 | from fastapi.staticfiles import StaticFiles |
13 | 13 | from mypy_extensions import TypedDict |
14 | 14 | from starlette.websockets import WebSocketDisconnect |
| 15 | +from uvicorn.config import Config as UvicornConfig |
| 16 | +from uvicorn.server import Server as UvicornServer |
| 17 | +from uvicorn.supervisors.multiprocess import Multiprocess |
| 18 | +from uvicorn.supervisors.statreload import StatReload as ChangeReload |
15 | 19 |
|
16 | 20 | from idom.config import IDOM_CLIENT_BUILD_DIR |
17 | 21 | from idom.core.dispatcher import ( |
@@ -42,10 +46,13 @@ class FastApiRenderServer(AbstractRenderServer[FastAPI, Config]): |
42 | 46 | """Base ``sanic`` extension.""" |
43 | 47 |
|
44 | 48 | _dispatcher_type: Type[AbstractDispatcher] |
| 49 | + _server: UvicornServer |
45 | 50 |
|
46 | | - def stop(self) -> None: |
| 51 | + def stop(self, timeout: float = 3) -> None: |
47 | 52 | """Stop the running application""" |
48 | | - self._loop.call_soon_threadsafe(self._loop.stop) |
| 53 | + self._server.should_exit |
| 54 | + if self._daemon_thread is not None: |
| 55 | + self._daemon_thread.join(timeout) |
49 | 56 |
|
50 | 57 | def _create_config(self, config: Optional[Config]) -> Config: |
51 | 58 | new_config: Config = { |
@@ -137,7 +144,10 @@ def _run_application( |
137 | 144 | args: Tuple[Any, ...], |
138 | 145 | kwargs: Dict[str, Any], |
139 | 146 | ) -> None: |
140 | | - uvicorn.run(app, host=host, port=port, loop="asyncio", *args, **kwargs) |
| 147 | + self._server = UvicornServer( |
| 148 | + UvicornConfig(app, host=host, port=port, loop="asyncio", *args, **kwargs) |
| 149 | + ) |
| 150 | + _run_uvicorn_server(self._server) |
141 | 151 |
|
142 | 152 | def _run_application_in_thread( |
143 | 153 | self, |
@@ -199,3 +209,35 @@ async def _run_dispatcher( |
199 | 209 | msg = f"SharedClientState server does not support per-client view parameters {params}" |
200 | 210 | raise ValueError(msg) |
201 | 211 | await self._dispatcher.run(send, recv, uuid.uuid4().hex, join=True) |
| 212 | + |
| 213 | + |
| 214 | +def _run_uvicorn_server(server: UvicornServer) -> None: |
| 215 | + # The following was copied from the uvicorn source with minimal modification. We |
| 216 | + # shouldn't need to do this, but unfortunately there's no easy way to gain access to |
| 217 | + # the server instance so you can stop it. |
| 218 | + # BUG: https://github.com/encode/uvicorn/issues/742 |
| 219 | + config = server.config |
| 220 | + |
| 221 | + if (config.reload or config.workers > 1) and not isinstance( |
| 222 | + server.config.app, str |
| 223 | + ): # pragma: no cover |
| 224 | + logger = logging.getLogger("uvicorn.error") |
| 225 | + logger.warning( |
| 226 | + "You must pass the application as an import string to enable 'reload' or " |
| 227 | + "'workers'." |
| 228 | + ) |
| 229 | + sys.exit(1) |
| 230 | + |
| 231 | + if config.should_reload: # pragma: no cover |
| 232 | + sock = config.bind_socket() |
| 233 | + supervisor = ChangeReload(config, target=server.run, sockets=[sock]) |
| 234 | + supervisor.run() |
| 235 | + elif config.workers > 1: # pragma: no cover |
| 236 | + sock = config.bind_socket() |
| 237 | + supervisor = Multiprocess(config, target=server.run, sockets=[sock]) |
| 238 | + supervisor.run() |
| 239 | + else: |
| 240 | + import asyncio |
| 241 | + |
| 242 | + asyncio.set_event_loop(asyncio.new_event_loop()) |
| 243 | + server.run() |
0 commit comments