diff --git a/src/backend/main.py b/src/backend/main.py index 6199b0f..9def165 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -1,6 +1,7 @@ import logging import logging.config +import asyncio import atexit import signal import sys @@ -37,7 +38,12 @@ def init_logger(): def cleanup() -> None: + logging.info('Cleaning up...') processes.terminate_subprocesses() + loop = asyncio.get_event_loop() + for task in asyncio.all_tasks(loop=loop): + task.cancel() + loop.stop() # Raises SystemExit sys.exit() diff --git a/src/backend/plugins/handler.py b/src/backend/plugins/handler.py index 892fbe9..1f7beb0 100644 --- a/src/backend/plugins/handler.py +++ b/src/backend/plugins/handler.py @@ -6,9 +6,11 @@ from api import expose from plugins.base_plugin import Plugin from plugins import priority +from utils.flag import Flag _PLUGINS: Dict[str, Plugin] = {} +_PLUGINS_UPDATED = Flag(True) def load_all() -> None: @@ -26,6 +28,7 @@ def load_all() -> None: def load(name: str) -> Optional[Plugin]: + _PLUGINS_UPDATED.set() try: source = f'plugins.plugins.{name}.backend.main' plugin: Plugin = importlib.import_module(source).init() @@ -81,6 +84,7 @@ def get_plugin_names() -> Tuple[str, ...]: def get_plugin_statuses() -> List[Dict[str, Any]]: + _PLUGINS_UPDATED.reset() result = [] for name, _ in priority.fetch_plugins(reload=False): if name in _PLUGINS: @@ -89,3 +93,11 @@ def get_plugin_statuses() -> List[Dict[str, Any]]: result.append({'name': name, 'status': False}) return result + + +def is_updated() -> bool: + return _PLUGINS_UPDATED.get() + + +def set_update_flag() -> None: + _PLUGINS_UPDATED.set() diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 8e0578a..2f6df7d 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -1,2 +1,4 @@ +asyncio fastapi -uvicorn[standard] \ No newline at end of file +sse_starlette +uvicorn[standard] diff --git a/src/backend/server/main.py b/src/backend/server/main.py index 37efa25..e30d957 100644 --- a/src/backend/server/main.py +++ b/src/backend/server/main.py @@ -1,12 +1,15 @@ -from typing import Any, Optional +from typing import Any, AsyncGenerator +import asyncio import datetime import inspect +import json import logging from fastapi import BackgroundTasks, FastAPI, Request, WebSocket from fastapi.responses import FileResponse, ORJSONResponse, PlainTextResponse from fastapi.staticfiles import StaticFiles +from sse_starlette.sse import EventSourceResponse from api import expose from db import users @@ -217,9 +220,21 @@ async def ws_plugins(websocket: WebSocket, plugin: str, endpoint: str) -> Any: return response -@app.get('/plugins/status', response_class=ORJSONResponse) -async def plugin_status(request: Request) -> ORJSONResponse: +@app.get('/plugins/status', response_class=EventSourceResponse) +async def plugin_status(request: Request) -> EventSourceResponse: if not database.uuid_exists(request.cookies.get('auth_cookie')): - return ORJSONResponse({'ok': False, 'error': 'Auth failed'}) + return EventSourceResponse(content=iter(('Authentication failed',)), status_code=401) + handler.set_update_flag() - return ORJSONResponse({'ok': True, 'plugins': handler.get_plugin_statuses()}) + async def event_generator() -> AsyncGenerator[str, None]: + while True: + if await request.is_disconnected(): + break + + if handler.is_updated(): + yield json.dumps( + {'ok': True, 'plugins': handler.get_plugin_statuses()} + ) + await asyncio.sleep(2.0) + + return EventSourceResponse(event_generator(), media_type='text/event-stream') diff --git a/src/backend/utils/flag.py b/src/backend/utils/flag.py new file mode 100644 index 0000000..0596950 --- /dev/null +++ b/src/backend/utils/flag.py @@ -0,0 +1,15 @@ +class Flag: + def __init__(self, default: bool = True) -> None: + self._flag = default + + def set(self) -> None: + self._flag = True + + def reset(self) -> None: + self._flag = False + + def get(self) -> bool: + return self._flag + + def __repr__(self) -> str: + return f'Flag({self._flag})' diff --git a/src/frontend/src/lib/components/plugins/PluginsContainer.svelte b/src/frontend/src/lib/components/plugins/PluginsContainer.svelte index 4486afe..74eb767 100644 --- a/src/frontend/src/lib/components/plugins/PluginsContainer.svelte +++ b/src/frontend/src/lib/components/plugins/PluginsContainer.svelte @@ -1,28 +1,36 @@ - {#if plugins.length == 0} diff --git a/src/frontend/src/lib/components/shared/api/plugins.type.ts b/src/frontend/src/lib/components/shared/api/plugins.type.ts index 09d8696..2ad3375 100644 --- a/src/frontend/src/lib/components/shared/api/plugins.type.ts +++ b/src/frontend/src/lib/components/shared/api/plugins.type.ts @@ -1,3 +1,8 @@ +export type PluginStatusContainer = { + ok: boolean; + plugins: Array; +}; + export type PluginStatus = { name: string; status: boolean; diff --git a/src/frontend/src/lib/components/shared/api/streams.ts b/src/frontend/src/lib/components/shared/api/streams.ts new file mode 100644 index 0000000..997c8b4 --- /dev/null +++ b/src/frontend/src/lib/components/shared/api/streams.ts @@ -0,0 +1,49 @@ +type CallbackFunction = (data: any) => void; +let pluginStatusStream = undefined; + +const createEventSource = ( + url: string, + defaultData: object +): { + subscribe: (listener: CallbackFunction) => () => boolean; + fetch: () => object; + close: () => void; +} => { + const eventSource = new EventSource(url); + const listeners: Set = new Set(); + let cachedData: object = defaultData; + + eventSource.onmessage = (event: MessageEvent) => { + cachedData = JSON.parse(event.data); + listeners.forEach((listener) => listener(cachedData)); + }; + + eventSource.onerror = () => { + const errorData = defaultData; + listeners.forEach((listener) => listener(errorData)); + }; + + return { + subscribe: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + close: () => { + eventSource.close(); + }, + fetch: () => { + return cachedData; + }, + }; +}; + +export function initEventSources() { + if (pluginStatusStream == undefined) { + pluginStatusStream = createEventSource("/plugins/status", { + ok: false, + plugins: [], + }); + } +} + +export { pluginStatusStream }; diff --git a/src/frontend/src/lib/components/shared/statusbar/Plugins.svelte b/src/frontend/src/lib/components/shared/statusbar/Plugins.svelte index 55700e4..663ca39 100644 --- a/src/frontend/src/lib/components/shared/statusbar/Plugins.svelte +++ b/src/frontend/src/lib/components/shared/statusbar/Plugins.svelte @@ -1,23 +1,34 @@