You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
7.8 KiB
231 lines
7.8 KiB
import asyncio |
|
import time |
|
import urllib.parse |
|
from pathlib import Path |
|
from typing import Dict, Optional |
|
|
|
from fastapi import HTTPException, Request |
|
from fastapi.middleware.gzip import GZipMiddleware |
|
from fastapi.responses import FileResponse, Response |
|
from fastapi.staticfiles import StaticFiles |
|
from fastapi_socketio import SocketManager |
|
|
|
from . import background_tasks, binding, favicon, globals, json, outbox, welcome # pylint: disable=redefined-builtin |
|
from .app import App |
|
from .client import Client |
|
from .dependencies import js_components, libraries |
|
from .error import error_content |
|
from .helpers import is_file, safe_invoke |
|
from .json import NiceGUIJSONResponse |
|
from .middlewares import RedirectWithPrefixMiddleware |
|
from .page import page |
|
from .version import __version__ |
|
|
|
globals.app = app = App(default_response_class=NiceGUIJSONResponse) |
|
# NOTE we use custom json module which wraps orjson |
|
socket_manager = SocketManager(app=app, mount_location='/_nicegui_ws/', json=json) |
|
globals.sio = sio = socket_manager._sio # pylint: disable=protected-access |
|
|
|
app.add_middleware(GZipMiddleware) |
|
app.add_middleware(RedirectWithPrefixMiddleware) |
|
static_files = StaticFiles( |
|
directory=(Path(__file__).parent / 'static').resolve(), |
|
follow_symlink=True, |
|
) |
|
app.mount(f'/_nicegui/{__version__}/static', static_files, name='static') |
|
|
|
globals.index_client = Client(page('/'), shared=True).__enter__() # pylint: disable=unnecessary-dunder-call |
|
|
|
|
|
@app.get('/') |
|
def index(request: Request) -> Response: |
|
return globals.index_client.build_response(request) |
|
|
|
|
|
@app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}') |
|
def get_library(key: str) -> FileResponse: |
|
is_map = key.endswith('.map') |
|
dict_key = key[:-4] if is_map else key |
|
if dict_key in libraries: |
|
path = libraries[dict_key].path |
|
if is_map: |
|
path = path.with_name(path.name + '.map') |
|
if path.exists(): |
|
headers = {'Cache-Control': 'public, max-age=3600'} |
|
return FileResponse(path, media_type='text/javascript', headers=headers) |
|
raise HTTPException(status_code=404, detail=f'library "{key}" not found') |
|
|
|
|
|
@app.get(f'/_nicegui/{__version__}' + '/components/{key:path}') |
|
def get_component(key: str) -> FileResponse: |
|
if key in js_components and js_components[key].path.exists(): |
|
headers = {'Cache-Control': 'public, max-age=3600'} |
|
return FileResponse(js_components[key].path, media_type='text/javascript', headers=headers) |
|
raise HTTPException(status_code=404, detail=f'component "{key}" not found') |
|
|
|
|
|
@app.on_event('startup') |
|
def handle_startup(with_welcome_message: bool = True) -> None: |
|
if not globals.ui_run_has_been_called: |
|
raise RuntimeError('\n\n' |
|
'You must call ui.run() to start the server.\n' |
|
'If ui.run() is behind a main guard\n' |
|
' if __name__ == "__main__":\n' |
|
'remove the guard or replace it with\n' |
|
' if __name__ in {"__main__", "__mp_main__"}:\n' |
|
'to allow for multiprocessing.') |
|
if globals.favicon: |
|
if is_file(globals.favicon): |
|
globals.app.add_route('/favicon.ico', lambda _: FileResponse(globals.favicon)) # type: ignore |
|
else: |
|
globals.app.add_route('/favicon.ico', lambda _: favicon.get_favicon_response()) |
|
else: |
|
globals.app.add_route('/favicon.ico', lambda _: FileResponse(Path(__file__).parent / 'static' / 'favicon.ico')) |
|
globals.state = globals.State.STARTING |
|
globals.loop = asyncio.get_running_loop() |
|
with globals.index_client: |
|
for t in globals.startup_handlers: |
|
safe_invoke(t) |
|
background_tasks.create(binding.loop()) |
|
background_tasks.create(outbox.loop()) |
|
background_tasks.create(prune_clients()) |
|
background_tasks.create(prune_slot_stacks()) |
|
globals.state = globals.State.STARTED |
|
if with_welcome_message: |
|
background_tasks.create(welcome.print_message()) |
|
if globals.air: |
|
background_tasks.create(globals.air.connect()) |
|
|
|
|
|
@app.on_event('shutdown') |
|
async def handle_shutdown() -> None: |
|
if app.native.main_window: |
|
app.native.main_window.signal_server_shutdown() |
|
globals.state = globals.State.STOPPING |
|
with globals.index_client: |
|
for t in globals.shutdown_handlers: |
|
safe_invoke(t) |
|
globals.state = globals.State.STOPPED |
|
if globals.air: |
|
await globals.air.disconnect() |
|
|
|
|
|
@app.exception_handler(404) |
|
async def exception_handler_404(request: Request, exception: Exception) -> Response: |
|
globals.log.warning(f'{request.url} not found') |
|
with Client(page('')) as client: |
|
error_content(404, exception) |
|
return client.build_response(request, 404) |
|
|
|
|
|
@app.exception_handler(Exception) |
|
async def exception_handler_500(request: Request, exception: Exception) -> Response: |
|
globals.log.exception(exception) |
|
with Client(page('')) as client: |
|
error_content(500, exception) |
|
return client.build_response(request, 500) |
|
|
|
|
|
@sio.on('handshake') |
|
def on_handshake(sid: str) -> bool: |
|
client = get_client(sid) |
|
if not client: |
|
return False |
|
client.environ = sio.get_environ(sid) |
|
sio.enter_room(sid, client.id) |
|
handle_handshake(client) |
|
return True |
|
|
|
|
|
def handle_handshake(client: Client) -> None: |
|
for t in client.connect_handlers: |
|
safe_invoke(t, client) |
|
for t in globals.connect_handlers: |
|
safe_invoke(t, client) |
|
|
|
|
|
@sio.on('disconnect') |
|
def on_disconnect(sid: str) -> None: |
|
client = get_client(sid) |
|
if not client: |
|
return |
|
handle_disconnect(client) |
|
|
|
|
|
def handle_disconnect(client: Client) -> None: |
|
if not client.shared: |
|
delete_client(client.id) |
|
for t in client.disconnect_handlers: |
|
safe_invoke(t, client) |
|
for t in globals.disconnect_handlers: |
|
safe_invoke(t, client) |
|
|
|
|
|
@sio.on('event') |
|
def on_event(sid: str, msg: Dict) -> None: |
|
client = get_client(sid) |
|
if not client or not client.has_socket_connection: |
|
return |
|
handle_event(client, msg) |
|
|
|
|
|
def handle_event(client: Client, msg: Dict) -> None: |
|
with client: |
|
sender = client.elements.get(msg['id']) |
|
if sender: |
|
msg['args'] = [None if arg is None else json.loads(arg) for arg in msg.get('args', [])] |
|
if len(msg['args']) == 1: |
|
msg['args'] = msg['args'][0] |
|
sender._handle_event(msg) # pylint: disable=protected-access |
|
|
|
|
|
@sio.on('javascript_response') |
|
def on_javascript_response(sid: str, msg: Dict) -> None: |
|
client = get_client(sid) |
|
if not client: |
|
return |
|
handle_javascript_response(client, msg) |
|
|
|
|
|
def handle_javascript_response(client: Client, msg: Dict) -> None: |
|
client.waiting_javascript_commands[msg['request_id']] = msg['result'] |
|
|
|
|
|
def get_client(sid: str) -> Optional[Client]: |
|
query_bytes: bytearray = sio.get_environ(sid)['asgi.scope']['query_string'] |
|
query = urllib.parse.parse_qs(query_bytes.decode()) |
|
client_id = query['client_id'][0] |
|
return globals.clients.get(client_id) |
|
|
|
|
|
async def prune_clients() -> None: |
|
while True: |
|
stale_clients = [ |
|
id |
|
for id, client in globals.clients.items() |
|
if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0 |
|
] |
|
for client_id in stale_clients: |
|
delete_client(client_id) |
|
await asyncio.sleep(10) |
|
|
|
|
|
async def prune_slot_stacks() -> None: |
|
while True: |
|
running = [ |
|
id(task) |
|
for task in asyncio.tasks.all_tasks() |
|
if not task.done() and not task.cancelled() |
|
] |
|
stale = [ |
|
id_ |
|
for id_ in globals.slot_stacks |
|
if id_ not in running |
|
] |
|
for id_ in stale: |
|
del globals.slot_stacks[id_] |
|
await asyncio.sleep(10) |
|
|
|
|
|
def delete_client(client_id: str) -> None: |
|
globals.clients.pop(client_id).remove_all_elements()
|
|
|