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 0000000..7bf23be Binary files /dev/null and b/frontend/src/pages/monitoring/term_font.png differ 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)