From 7fa635170a6acb395391ae3d4dec6fe673d52cda Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Thu, 22 Sep 2022 19:27:01 +0200 Subject: [PATCH] Implement monitoring infrastructure --- frontend/src/App.svelte | 16 ++- frontend/src/Navlink.svelte | 2 +- frontend/src/app.scss | 9 +- .../src/pages/monitoring/Monitoring.svelte | 27 ++++ frontend/src/pages/monitoring/Screen.svelte | 121 ++++++++++++++++++ frontend/src/pages/monitoring/Viewer.svelte | 65 ++++++++++ frontend/src/pages/monitoring/font.ts | 25 ++++ frontend/src/pages/monitoring/proto.ts | 12 ++ frontend/src/pages/monitoring/term_font.png | Bin 0 -> 3904 bytes frontend/src/pages/playground/Bar.svelte | 21 +++ frontend/src/pages/playground/Canvas.svelte | 24 ++++ lua/main.tl | 2 +- server/dummy-client.py | 96 ++++++++++++++ server/server/__init__.py | 11 +- server/server/monitoring.py | 97 +++++++++++--- 15 files changed, 499 insertions(+), 29 deletions(-) create mode 100644 frontend/src/pages/monitoring/Monitoring.svelte create mode 100644 frontend/src/pages/monitoring/Screen.svelte create mode 100644 frontend/src/pages/monitoring/Viewer.svelte create mode 100644 frontend/src/pages/monitoring/font.ts create mode 100644 frontend/src/pages/monitoring/proto.ts create mode 100644 frontend/src/pages/monitoring/term_font.png create mode 100644 frontend/src/pages/playground/Bar.svelte create mode 100644 frontend/src/pages/playground/Canvas.svelte create mode 100644 server/dummy-client.py diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index f6913ef..4ab0ec1 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -9,6 +9,8 @@ import Mining from "./pages/mining/Mining.svelte"; import Footer from "./Footer.svelte"; import BaseLayout from "./BaseLayout.svelte"; + import Monitoring from "./pages/monitoring/Monitoring.svelte"; + import Bar from "./pages/playground/Bar.svelte"; onMount(async () => { const res = await fetch("/user/me"); @@ -25,7 +27,7 @@ - +
@@ -38,19 +40,19 @@ - - bar + + - - monitoring + + - + - + stats diff --git a/frontend/src/Navlink.svelte b/frontend/src/Navlink.svelte index bfee97a..0c567ce 100644 --- a/frontend/src/Navlink.svelte +++ b/frontend/src/Navlink.svelte @@ -10,7 +10,7 @@ diff --git a/frontend/src/app.scss b/frontend/src/app.scss index 53bb026..b2eb317 100644 --- a/frontend/src/app.scss +++ b/frontend/src/app.scss @@ -18,6 +18,11 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts"; @import "@fortawesome/fontawesome-free/scss/v4-shims.scss"; // https://github.com/mefechoel/svelte-navigator#what-are-the-weird-rectangles-around-the-headings-in-my-app -h1:focus { +h1:focus, +h2:focus, +h3:focus, +h4:focus, +h5:focus, +h6:focus { outline: none; -} +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/Monitoring.svelte b/frontend/src/pages/monitoring/Monitoring.svelte new file mode 100644 index 0000000..0701ece --- /dev/null +++ b/frontend/src/pages/monitoring/Monitoring.svelte @@ -0,0 +1,27 @@ + + +
+ +
+

Yo wassup

+
    + {#each uuids as id} +
  • {id}
  • + {/each} +
+
+
+ + +
+

ASSUMING DIRECT CONTROL

+ + fuck go back +
+
+
diff --git a/frontend/src/pages/monitoring/Screen.svelte b/frontend/src/pages/monitoring/Screen.svelte new file mode 100644 index 0000000..baa94ad --- /dev/null +++ b/frontend/src/pages/monitoring/Screen.svelte @@ -0,0 +1,121 @@ + + + diff --git a/frontend/src/pages/monitoring/Viewer.svelte b/frontend/src/pages/monitoring/Viewer.svelte new file mode 100644 index 0000000..3622cb0 --- /dev/null +++ b/frontend/src/pages/monitoring/Viewer.svelte @@ -0,0 +1,65 @@ + + + +
{JSON.stringify(data, null, 2)}
diff --git a/frontend/src/pages/monitoring/font.ts b/frontend/src/pages/monitoring/font.ts new file mode 100644 index 0000000..df4fad7 --- /dev/null +++ b/frontend/src/pages/monitoring/font.ts @@ -0,0 +1,25 @@ +export const charWidth = 6, charHeight = 9; + +export async function loadFont(src: RequestInfo | URL): Promise { + const fontImg = await fetch(src); + const fontBlob = await fontImg.blob(); + + async function getCharBitmap(x: number, y: number) { + const fontOffsetX = 1, fontOffsetY = 1, fontPaddingX = 2, fontPaddingY = 2; + + const offsetX = (charWidth + fontPaddingX) * x + fontOffsetX; + const offsetY = (charHeight + fontPaddingY) * y + fontOffsetY; + + return await createImageBitmap(fontBlob, offsetX, offsetY, charWidth, charHeight); + } + + const font = Array(256); + + for (let y = 0; y < 16; ++y) { + for (let x = 0; x < 16; ++x) { + const i = 16 * y + x; + font[i] = getCharBitmap(x, y); + } + } + return await Promise.all(font); +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/proto.ts b/frontend/src/pages/monitoring/proto.ts new file mode 100644 index 0000000..2eb5029 --- /dev/null +++ b/frontend/src/pages/monitoring/proto.ts @@ -0,0 +1,12 @@ +export type ScreenContent = { + x: number + y: number + width: number + height: number + blink: boolean + fg: number + text: string[] + fg_color: string[] + bg_color: string[] + palette: number[] +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/term_font.png b/frontend/src/pages/monitoring/term_font.png new file mode 100644 index 0000000000000000000000000000000000000000..7bf23be2e751a8ba4dc8f408e56f0522198e6d2d GIT binary patch literal 3904 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 XA!+xD{uor-`QTWevb7+adEfgF>y~f^ literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playground/Bar.svelte b/frontend/src/pages/playground/Bar.svelte new file mode 100644 index 0000000..3f756a7 --- /dev/null +++ b/frontend/src/pages/playground/Bar.svelte @@ -0,0 +1,21 @@ + + +
+ +
diff --git a/frontend/src/pages/playground/Canvas.svelte b/frontend/src/pages/playground/Canvas.svelte new file mode 100644 index 0000000..748ed3f --- /dev/null +++ b/frontend/src/pages/playground/Canvas.svelte @@ -0,0 +1,24 @@ + + + +
color: {color}
+redraws: {redraws}
diff --git a/lua/main.tl b/lua/main.tl index ec6ca82..ccbe073 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -2,7 +2,7 @@ local json = require("json") local fb = require("framebuffer") local ringbuffer = require("ringbuffer") local UUID = "8b9faf9f-9470-4a50-b405-0af5f0152550" -local ENDPOINT = "ws://localhost:8000/monitoring/computer/" .. UUID .. "/ws" +local ENDPOINT = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws" print("[MAIN] Init") diff --git a/server/dummy-client.py b/server/dummy-client.py new file mode 100644 index 0000000..d9cc40b --- /dev/null +++ b/server/dummy-client.py @@ -0,0 +1,96 @@ +import asyncio +import json +import websockets + +ENDPOINT = "ws://localhost:8000/ipmi/computer/8b9faf9f-9470-4a50-b405-0af5f0152550/ws" + + +def gen_payload(tick: int): + return { + "x": 3, + "y": 4, + "width": 39, + "height": 13, + "blink": True, + "fg": 0, + "text": [ + "[WS] OK\u0003 ", + "FG 0123456789ABCDEF ", + "BG 0123456789ABCDEF ", + " ", + f"Tick: {tick:8d} ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + "fg_color": [ + "78870dd50000000000000000000000000000000", + "0000123456789abcdef00000000000000000000", + "000f00000000000000000000000000000000000", + "440000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + ], + "bg_color": [ + "fffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffff0ffffffffffffffffffff", + "fff0123456789abcdefffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + ], + "palette": [ + 15790320, + 15905331, + 15040472, + 10072818, + 14605932, + 8375321, + 15905484, + 5000268, + 10066329, + 5020082, + 11691749, + 3368652, + 8349260, + 5744206, + 13388876, + 1118481, + ], + } + + +async def main(): + tick = 0 + + async with websockets.connect(ENDPOINT) as socket: + while True: + await socket.send(json.dumps({"screen": gen_payload(tick)})) + await asyncio.sleep(1 / 20) + tick += 1 + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("this handler gets it") diff --git a/server/server/__init__.py b/server/server/__init__.py index 2b52354..7ae8c91 100644 --- a/server/server/__init__.py +++ b/server/server/__init__.py @@ -1,3 +1,4 @@ +import asyncio import json from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse @@ -6,14 +7,20 @@ from .settings import settings from .user import user_auth from .map_tiles import map_tiles, map_meta from .templates import j2env -from .monitoring import monitoring +from .monitoring import monitoring, ws_manager app = FastAPI() app.mount("/user/", user_auth) app.mount("/map/", map_meta) app.mount("/tiles/", map_tiles) -app.mount("/monitoring/", monitoring) +app.mount("/ipmi/", monitoring) + + +@app.on_event("startup") +async def on_startup(): + asyncio.get_running_loop().create_task(ws_manager.queue_task()) + frontend = FastAPI() diff --git a/server/server/monitoring.py b/server/server/monitoring.py index a55fd56..6801f58 100644 --- a/server/server/monitoring.py +++ b/server/server/monitoring.py @@ -1,10 +1,13 @@ +import asyncio import json from uuid import UUID from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError + monitoring = FastAPI() + class ScreenContent(BaseModel): x: int y: int @@ -15,24 +18,86 @@ class ScreenContent(BaseModel): text: list[str] fg_color: list[str] bg_color: list[str] + palette: list[int] -class Ping(BaseModel): - screen: ScreenContent -@monitoring.post("/ping") -async def ping(request: Request, data: Ping): - print("[PING]") - for line in data.screen.text: - print(line) +class Update(BaseModel): + screen: ScreenContent | None + + +class WSManager: + def __init__(self): + self.computers: dict[UUID, WebSocket] = dict() + self.viewers: dict[UUID, set[WebSocket]] = dict() + self.queue: asyncio.Queue[tuple[UUID, any]] = asyncio.Queue() + + async def queue_task(self): + print("[WS] queue task started") + while True: + (uuid, message) = await self.queue.get() + + if uuid not in self.viewers: + continue + + viewers = self.viewers[uuid] + await asyncio.gather(*(viewer.send_json(message) for viewer in viewers)) + + async def broadcast(self, uuid: UUID, message): + await self.queue.put((uuid, message)) + + async def on_computer_connect(self, socket: WebSocket, uuid: UUID): + if uuid in self.computers: + print(f"[WS] Closing duplicate connection for {uuid}") + await socket.close() + return + + print(f"[WS] Computer {uuid} connected") + self.computers[uuid] = socket + while True: + try: + data = await socket.receive_json() + data = Update.parse_obj(data) + + if data.screen: + await self.broadcast(uuid, data.screen.dict()) + + except ValidationError as e: + print(f"[WS] Received invalid message from {uuid}:") + print(e.json) + except WebSocketDisconnect: + break + + del self.computers[uuid] + print(f"[WS] Computer {uuid} disconnected") + + async def on_browser_connect(self, socket: WebSocket, uuid: UUID): + print(f"[WS] Browser connected for {uuid}") + + if uuid not in self.viewers: + self.viewers[uuid] = set() + + self.viewers[uuid].add(socket) + + while True: + try: + data = await socket.receive_json() + except WebSocketDisconnect: + break + self.viewers[uuid].remove(socket) + print(f"[WS] Browser disconnected for {uuid}") + + +ws_manager = WSManager() + + @monitoring.websocket("/computer/{uuid}/ws") async def computer_ws(socket: WebSocket, uuid: UUID): await socket.accept() - print(f"[WS] Computer {uuid} connected") - while True: - try: - data = await socket.receive_json() - #print(f"[WS] rx {json.dumps(data)}") - except WebSocketDisconnect: - break - print(f"[WS] Computer {uuid} disconnected") \ No newline at end of file + await ws_manager.on_computer_connect(socket, uuid) + + +@monitoring.websocket("/browser/{uuid}/ws") +async def browser_ws(socket: WebSocket, uuid: UUID): + await socket.accept() + await ws_manager.on_browser_connect(socket, uuid)