sanified app loading

This commit is contained in:
Jakob Pinterits
2024-10-24 22:03:53 +02:00
parent 5b000d8363
commit a3de7f63a0
4 changed files with 55 additions and 83 deletions

View File

@@ -201,8 +201,6 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
base_url=base_url,
)
self.internal_on_app_start = internal_on_app_start
# If a URL was provided, run some sanity checks
if base_url is not None:
# If the URL is missing a protocol, yarl doesn't consider it
@@ -228,8 +226,8 @@ class FastapiServer(fastapi.FastAPI, AbstractAppServer):
"The app's base URL cannot contain a fragment."
)
# All good
self.base_url = base_url
self.internal_on_app_start = internal_on_app_start
self.base_url = base_url
# While this Event is unset, no new Sessions can be created. This is
# used to ensure that no clients (re-)connect while `rio run` is

View File

@@ -11,8 +11,8 @@ import path_imports
import revel
import rio
import rio.global_state
import rio.app_server.fastapi_server
import rio.global_state
from rio import icon_registry
from ... import project_config
@@ -211,13 +211,15 @@ def import_app_module(
def load_user_app(
proj: project_config.RioProjectConfig,
) -> tuple[
rio.App,
rio.app_server.fastapi_server.FastapiServer | None,
]:
) -> rio.app_server.fastapi_server.FastapiServer:
"""
Load and return the user app. Raises `AppLoadError` if the app can't be
Load and return the user's app. Raises `AppLoadError` if the app can't be
loaded for whichever reason.
The result is actually an app server, rather than just the app. This is done
so the user can use `as_fastapi` on a Rio app and still use `rio run` to run
that app. If you actually need the app object, it's stored inside the app
server.
"""
# Import the app module
try:
@@ -268,12 +270,14 @@ def load_user_app(
if len(as_fastapi_apps) > 0:
app_list = as_fastapi_apps
app_server = as_fastapi_apps[0][1]
app_instance = app_server.app
# Case: Rio app
elif len(rio_apps) > 0:
app_list = rio_apps
app_server = None
app_instance = rio_apps[0][1]
app_server = app_instance.as_fastapi()
assert isinstance(
app_server, rio.app_server.fastapi_server.FastapiServer
)
# Case: No app
else:
raise AppLoadError(
@@ -291,4 +295,4 @@ def load_user_app(
f"{main_file_reference} defines multiple Rio apps: {variables_string}. Please make sure there is exactly one."
)
return app_instance, app_server
return app_server

View File

@@ -292,16 +292,16 @@ class Arbiter:
def try_load_app(
self,
) -> tuple[
rio.App,
rio.app_server.fastapi_server.FastapiServer | None,
rio.app_server.fastapi_server.FastapiServer,
Exception | None,
]:
"""
Tries to load the user's app. If it fails, a dummy app is created and
returned, unless running in release mode.
returned, unless running in release mode. (In release mode screams and
exits the entire process.)
Returns the app instance, the app's server instance and an exception if
the app could not be loaded.
Returns the app server instance and an exception if the app could not be
loaded.
The app server is returned in case the user has called `as_fastapi` on
their app instance. In that case the actual fastapi app should be
@@ -310,7 +310,7 @@ class Arbiter:
rio.cli._logger.debug("Trying to load the app")
try:
app, app_server = app_loading.load_user_app(self.proj)
app_server = app_loading.load_user_app(self.proj)
except app_loading.AppLoadError as err:
if err.__cause__ is not None:
@@ -327,21 +327,23 @@ class Arbiter:
# Otherwise create a placeholder app which displays the error
# message
return (
app_loading.make_error_message_app(
err,
self.proj.project_directory,
self._app_theme,
),
None,
app = app_loading.make_error_message_app(
err,
self.proj.project_directory,
self._app_theme,
)
app_server = app.as_fastapi()
assert isinstance(
app_server, rio.app_server.fastapi_server.FastapiServer
)
return app_server, err
# Remember the app's theme. If in the future a placeholder app is used,
# this theme will be used for it.
self._app_theme = app._theme
self._app_theme = app_server.app._theme
return app, app_server, None
return app_server, None
def run(self) -> None:
assert not self._stop_requested.is_set()
@@ -517,7 +519,7 @@ class Arbiter:
apply_monkeypatches()
# Try to load the app
app, app_server, _ = self.try_load_app()
app_server, _ = self.try_load_app()
# Start the file watcher
if self.debug_mode:
@@ -539,7 +541,6 @@ class Arbiter:
self._uvicorn_worker = uvicorn_worker.UvicornWorker(
push_event=self.push_event,
app=app,
app_server=app_server,
socket=sock,
quiet=self.quiet,
@@ -744,10 +745,10 @@ window.setConnectionLostPopupVisible(true);
await app_server._call_on_app_close()
# Load the user's app again
new_app, new_app_server, loading_error = self.try_load_app()
new_app_server, loading_error = self.try_load_app()
# Replace the app which is currently hosted by uvicorn
self._uvicorn_worker.replace_app(new_app, new_app_server)
self._uvicorn_worker.replace_app(new_app_server)
# The app has changed, but the uvicorn server is still the same.
# Because of this, uvicorn won't call the `on_app_start` function -

View File

@@ -21,8 +21,7 @@ class UvicornWorker:
self,
*,
push_event: t.Callable[[run_models.Event], None],
app: rio.App,
app_server: rio.app_server.fastapi_server.FastapiServer | None,
app_server: rio.app_server.fastapi_server.FastapiServer,
socket: socket.socket,
quiet: bool,
debug_mode: bool,
@@ -40,38 +39,15 @@ class UvicornWorker:
# The app server used to host the app.
#
# This can optionally be provided to the constructor. If not, it will be
# created when the worker is started. This allows for the app to be
# either a Rio app or a FastAPI app (derived from a Rio app).
self.app = app
self.app_server: rio.app_server.fastapi_server.FastapiServer | None = (
None
)
self.replace_app(app, 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
)
# While already provided in the constructor, this needs some values to
# be overridden. `replace_app` already handles this, so delegate to that
# function.
self.app_server = app_server
self.replace_app(app_server)
async def run(self) -> None:
rio.cli._logger.debug("Uvicorn worker is starting")
# Create the app server
if self.app_server is None:
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
@@ -153,36 +129,29 @@ class UvicornWorker:
def replace_app(
self,
app: rio.App,
app_server: rio.app_server.fastapi_server.FastapiServer | None,
app_server: rio.app_server.fastapi_server.FastapiServer,
) -> None:
"""
Replace the app currently running in the server with a new one. The
worker must already be running for this to work.
"""
assert (
app_server.internal_on_app_start is None
), app_server.internal_on_app_start
# Store the new app
self.app = app
self.app_server = app_server
# And create a new app server. This is necessary, because the mounted
# sub-apps may have changed. This ensures they're up to date.
if app_server is None:
self._create_and_store_app_server()
self.app_server.debug_mode = self.debug_mode
self.app_server.running_in_window = self.run_in_window
self.app_server.internal_on_app_start = (
lambda: self.on_server_is_ready_or_failed.set_result(None)
)
if self.base_url is None:
self.app_server.base_url = None
else:
self.app_server = app_server
self.app_server.debug_mode = self.debug_mode
self.app_server.running_in_window = self.run_in_window
self.app_server.internal_on_app_start = (
lambda: self.on_server_is_ready_or_failed.set_result(None)
)
if self.base_url is None:
self.app_server.base_url = None
else:
self.app_server.base_url = utils.normalize_url(self.base_url)
assert self.app_server is not None
assert self.app_server.app is self.app
self.app_server.base_url = utils.normalize_url(self.base_url)
# 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