diff --git a/rio/app_server/fastapi_server.py b/rio/app_server/fastapi_server.py index 6b91ba02..1a82d1e6 100644 --- a/rio/app_server/fastapi_server.py +++ b/rio/app_server/fastapi_server.py @@ -326,15 +326,26 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer): # The route that serves the index.html will be registered later, so that # it has a lower priority than user-created routes. + # + # This keeps track of whether the fallback route has already been + # registered. + self._index_hmtl_route_registered = False async def __call__(self, scope, receive, send) -> None: # Because this is a single page application, all other routes should # serve the index page. The session will determine which components # should be shown. - self.add_api_route( - "/{initial_route_str:path}", self._serve_index, methods=["GET"] - ) + # + # This route is registered last, so that it has the lowest priority. + # This allows the user to add custom routes that take precedence. + if not self._index_hmtl_route_registered: + self._index_hmtl_route_registered = True + self.add_api_route( + "/{initial_route_str:path}", self._serve_index, methods=["GET"] + ) + + # Delegate to FastAPI return await super().__call__(scope, receive, send) @contextlib.asynccontextmanager diff --git a/rio/cli/run_project/app_loading.py b/rio/cli/run_project/app_loading.py index d31d9d36..544182d0 100644 --- a/rio/cli/run_project/app_loading.py +++ b/rio/cli/run_project/app_loading.py @@ -253,7 +253,7 @@ def load_user_app( if isinstance(var, rio.app_server.fastapi_server.FastapiServer): as_fastapi_apps.append((var_name, var)) - if isinstance(var, rio.App): + elif isinstance(var, rio.App): rio_apps.append((var_name, var)) # Prepare the main file name @@ -262,10 +262,12 @@ def load_user_app( else: main_file_reference = f"The file `{Path(app_module.__file__).relative_to(proj.project_directory)}`" + print(as_fastapi_apps, rio_apps) + # Which type of app do we have? # # Case: FastAPI app - if len(as_fastapi_apps) > 1: + if len(as_fastapi_apps) > 0: app_list = as_fastapi_apps app_server = as_fastapi_apps[0][1] app_instance = app_server.app diff --git a/rio/cli/run_project/uvicorn_worker.py b/rio/cli/run_project/uvicorn_worker.py index 43a6c591..aa0b1ec0 100644 --- a/rio/cli/run_project/uvicorn_worker.py +++ b/rio/cli/run_project/uvicorn_worker.py @@ -5,6 +5,7 @@ import typing as t import revel import uvicorn import uvicorn.lifespan.on +from starlette.types import Receive, Scope, Send import rio import rio.app_server.fastapi_server @@ -44,27 +45,47 @@ class UvicornWorker: # either a Rio app or a FastAPI app (derived from a Rio app). self.app_server = app_server + def _create_and_store_app_server(self) -> None: + app_server = self.app._as_fastapi( + debug_mode=self.debug_mode, + running_in_window=self.run_in_window, + internal_on_app_start=lambda: self.on_server_is_ready_or_failed.set_result( + None + ), + base_url=self.base_url, + ) + assert isinstance( + app_server, rio.app_server.fastapi_server.FastapiServer + ) + self.app_server = app_server + async def run(self) -> None: rio.cli._logger.debug("Uvicorn worker is starting") - # Set up a uvicorn server, but don't start it yet + # Create the app server if self.app_server is None: - app_server = self.app._as_fastapi( - debug_mode=self.debug_mode, - running_in_window=self.run_in_window, - internal_on_app_start=lambda: self.on_server_is_ready_or_failed.set_result( - None - ), - base_url=self.base_url, - ) - assert isinstance( - app_server, rio.app_server.fastapi_server.FastapiServer - ) - self.app_server = app_server - del app_server + self._create_and_store_app_server() + assert self.app_server is not None + # Instead of using the ASGI app directly, create a transparent shim that + # redirect's to the worker's currently stored app server. This allows + # replacing the app server at will because the shim always remains the + # same. + # + # ASGI is a bitch about function signatures. This function cannot be a + # simple method, because the added `self` parameter seems to confused + # whoever the caller is. Hence the nested function. + async def _asgi_shim( + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + assert self.app_server is not None + await self.app_server(scope, receive, send) + + # Set up a uvicorn server, but don't start it yet config = uvicorn.Config( - self.app_server, + app=_asgi_shim, log_config=None, # Prevent uvicorn from configuring global logging log_level="error" if self.quiet else "info", timeout_graceful_shutdown=1, # Without a timeout the server sometimes deadlocks @@ -135,7 +156,14 @@ class UvicornWorker: worker must already be running for this to work. """ assert self.app_server is not None - rio.cli._logger.debug("Replacing the app in the server") - self.app_server.app = app - # TODO: What to do with the new server here? + # Store the new app + self.app = app + + # And create a new app server. This is necessary, because the mounted + # sub-apps may have changed. This ensures they're up to date. + self._create_and_store_app_server() + + # There is no need to inject the new app or server anywhere. Since + # uvicorn was fed a shim function instead of the app directly, any + # requests will automatically be redirected to the new server instance.