Skip to content

Commit

Permalink
Fallback server option
Browse files Browse the repository at this point in the history
This adds the option to start a basic fallback server that
shows an error page.
  • Loading branch information
rwb27 committed Aug 2, 2024
1 parent e519a6f commit a653f43
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 7 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dev = [
]
server = [
"fastapi[all]>=0.104.0", # NB must match FastAPI above
"zeroconf >=0.28.0",
]

[project.urls]
Expand Down
6 changes: 5 additions & 1 deletion src/labthings_fastapi/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, settings_folder: Optional[str] = None):
self.add_things_view_to_app()
self._things: dict[str, Thing] = {}
self.blocking_portal: Optional[BlockingPortal] = None
self.startup_status: dict[str, str | dict] = {"things": {}}
global _thing_servers
_thing_servers.add(self)

Expand Down Expand Up @@ -176,7 +177,10 @@ def server_from_config(config: dict) -> ThingServer:
f"specified as the class for {path}. The error is "
f"printed below:\n\n{e}"
)
instance = cls(*thing.get("args", {}), **thing.get("kwargs", {}))
try:
instance = cls(*thing.get("args", {}), **thing.get("kwargs", {}))
except Exception as e:
raise e
assert isinstance(instance, Thing), f"{thing['class']} is not a Thing"
server.add_thing(instance, path)
return server
34 changes: 28 additions & 6 deletions src/labthings_fastapi/server/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from argparse import ArgumentParser, Namespace
import json

from labthings_fastapi.utilities.object_reference_to_object import (
object_reference_to_object,
)
import uvicorn

from . import ThingServer, server_from_config
Expand All @@ -11,6 +14,11 @@ def parse_args(argv: list[str] | None = None) -> Namespace:
parser = ArgumentParser()
parser.add_argument("-c", "--config", type=str, help="Path to configuration file")
parser.add_argument("-j", "--json", type=str, help="Configuration as JSON string")
parser.add_argument(
"--fallback",
action="store_true",
help="Serve an error page instead of exiting, if we can't start.",
)
parser.add_argument(
"--host", type=str, default="127.0.0.1", help="Bind socket to this host"
)
Expand Down Expand Up @@ -46,9 +54,23 @@ def config_from_args(args: Namespace) -> dict:
def serve_from_cli(argv: list[str] | None = None, dry_run=False):
"""Start the server from the command line"""
args = parse_args(argv)
config = config_from_args(args)
server = server_from_config(config)
assert isinstance(server, ThingServer)
if dry_run:
return server
uvicorn.run(server.app, host=args.host, port=args.port)
try:
config, server = None, None
config = config_from_args(args)
server = server_from_config(config)
assert isinstance(server, ThingServer)
if dry_run:
return server
uvicorn.run(server.app, host=args.host, port=args.port)
except BaseException as e:
if args.fallback:
print(f"Error: {e}")
fallback_server = "labthings_fastapi.server.fallback:app"
print(f"Starting fallback server {fallback_server}.")
app = object_reference_to_object(fallback_server)
app.labthings_config = config
app.labthings_server = server
app.labthings_error = e
uvicorn.run(app, host=args.host, port=args.port)
else:
raise e
56 changes: 56 additions & 0 deletions src/labthings_fastapi/server/fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import json
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from starlette.responses import RedirectResponse

app = FastAPI()

ERROR_PAGE = """
<!DOCTYPE html>
<html>
<head>
<title>LabThings</title>
</head>
<body>
<h1>LabThings Could't Load</h1>
<p>Something went wrong when setting up your LabThings server.</p>
<p>Please check your configuration and try again.</p>
<p>More details may be shown below:</p>
<pre>{{error}}</pre>
<p>The following Things loaded successfuly:</p>
<ul>
{{things}}
</ul>
<p>Your configuration:</p>
<pre>
{{config}}
</pre>
</body>
</html>
"""

app.labthings_config = None
app.labthings_server = None
app.labthings_error = None


@app.get("/")
async def root():
error_message = f"{app.labthings_error!r}"
things = ""
if app.labthings_server:
for path, thing in app.labthings_server.things.items():
things += f"<li>{path}: {thing!r}</li>"

content = ERROR_PAGE
content = content.replace("{{error}}", error_message)
content = content.replace("{{things}}", things)
content = content.replace("{{config}}", json.dumps(app.labthings_config, indent=2))
return HTMLResponse(
content=ERROR_PAGE.replace("{{error}}", app.error_message), status_code=500
)


@app.get("/{path:path}")
async def redirect_to_root(path: str):
return RedirectResponse(url="/")
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,40 @@ def test_serve_with_no_config():
check_serve_from_cli([])


def test_invalid_thing_and_fallback():
"""Check it fails for invalid things, and test the fallback option"""
config_json = json.dumps(
{
"things": {
"broken": "labthings_fastapi.example_things:MissingThing",
}
}
)
with raises(ImportError):
check_serve_from_cli(["-j", config_json])
## the line below should start a dummy server with an error page -
## it terminates happily once the server starts.
check_serve_from_cli(["-j", config_json, "--fallback"])


def test_invalid_config():
"""Check it fails for invalid config"""
with raises(FileNotFoundError):
check_serve_from_cli(["-c", "non_existent_file.json"])


def test_thing_that_cant_start():
"""Check it fails for a thing that can't start"""
config_json = json.dumps(
{
"things": {
"broken": "labthings_fastapi.example_things:ThingThatCantStart",
}
}
)
with raises(SystemExit):
check_serve_from_cli(["-j", config_json])


if __name__ == "__main__":
test_serve_from_cli_with_config_json()

0 comments on commit a653f43

Please sign in to comment.