Implement monitoring infrastructure
This commit is contained in:
parent
2af0136703
commit
7fa635170a
@ -9,6 +9,8 @@
|
|||||||
import Mining from "./pages/mining/Mining.svelte";
|
import Mining from "./pages/mining/Mining.svelte";
|
||||||
import Footer from "./Footer.svelte";
|
import Footer from "./Footer.svelte";
|
||||||
import BaseLayout from "./BaseLayout.svelte";
|
import BaseLayout from "./BaseLayout.svelte";
|
||||||
|
import Monitoring from "./pages/monitoring/Monitoring.svelte";
|
||||||
|
import Bar from "./pages/playground/Bar.svelte";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const res = await fetch("/user/me");
|
const res = await fetch("/user/me");
|
||||||
@ -25,7 +27,7 @@
|
|||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/foo">
|
<Route path="foo">
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<section class="hero is-danger is-fullheight">
|
<section class="hero is-danger is-fullheight">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
@ -38,19 +40,19 @@
|
|||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/bar">
|
<Route path="bar">
|
||||||
<BaseLayout>bar</BaseLayout>
|
<BaseLayout><Bar /></BaseLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/monitoring">
|
<Route path="monitoring/*">
|
||||||
<BaseLayout>monitoring</BaseLayout>
|
<BaseLayout><Monitoring /></BaseLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/mining">
|
<Route path="mining">
|
||||||
<Mining />
|
<Mining />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/stats">
|
<Route path="stats">
|
||||||
<BaseLayout>stats</BaseLayout>
|
<BaseLayout>stats</BaseLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={target}
|
to={target}
|
||||||
class="navbar-item is-tab {$location.pathname === target
|
class="navbar-item is-tab {$location.pathname.startsWith(target)
|
||||||
? 'is-active'
|
? 'is-active'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
|
@ -18,6 +18,11 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
|
|||||||
@import "@fortawesome/fontawesome-free/scss/v4-shims.scss";
|
@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
|
// 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;
|
outline: none;
|
||||||
}
|
}
|
27
frontend/src/pages/monitoring/Monitoring.svelte
Normal file
27
frontend/src/pages/monitoring/Monitoring.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Link, Route, useParams } from "svelte-navigator";
|
||||||
|
import Viewer from "./Viewer.svelte";
|
||||||
|
|
||||||
|
const uuids = ["8b9faf9f-9470-4a50-b405-0af5f0152550"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<Route path="/">
|
||||||
|
<div class="content">
|
||||||
|
<p>Yo wassup</p>
|
||||||
|
<ul>
|
||||||
|
{#each uuids as id}
|
||||||
|
<li><Link to="./{id}">{id}</Link></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path=":id" let:params>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">ASSUMING DIRECT CONTROL</h1>
|
||||||
|
<Viewer uuid={params.id} />
|
||||||
|
<Link to="..">fuck go back</Link>
|
||||||
|
</div>
|
||||||
|
</Route>
|
||||||
|
</section>
|
121
frontend/src/pages/monitoring/Screen.svelte
Normal file
121
frontend/src/pages/monitoring/Screen.svelte
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import termFont from "./term_font.png";
|
||||||
|
import { loadFont, charWidth, charHeight } from "./font";
|
||||||
|
import type { ScreenContent } from "./proto";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
|
||||||
|
export let content: ScreenContent;
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let composeCanvas: HTMLCanvasElement;
|
||||||
|
let font: ImageBitmap[];
|
||||||
|
|
||||||
|
const paddingX = 2,
|
||||||
|
paddingY = 2;
|
||||||
|
|
||||||
|
const toColor = (n: number) => `#${n.toString(16).padStart(6, "0")}`;
|
||||||
|
|
||||||
|
// SAFETY: Only called if all of (content, canvas, composeCanvas, font) are defined
|
||||||
|
function drawChar(
|
||||||
|
charCode: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
textColor: number,
|
||||||
|
backgroundColor: number
|
||||||
|
) {
|
||||||
|
let cx = canvas.getContext("2d");
|
||||||
|
let ccx = composeCanvas.getContext("2d");
|
||||||
|
|
||||||
|
const offsetX = paddingX + x * charWidth;
|
||||||
|
const offsetY = paddingY + y * charHeight;
|
||||||
|
|
||||||
|
let x0 = offsetX;
|
||||||
|
let y0 = offsetY;
|
||||||
|
let w = charWidth;
|
||||||
|
let h = charHeight;
|
||||||
|
|
||||||
|
// handle padding
|
||||||
|
if (x === 0) {
|
||||||
|
x0 -= paddingX;
|
||||||
|
w += paddingX;
|
||||||
|
} else if (x === content.width - 1) {
|
||||||
|
w += paddingX;
|
||||||
|
}
|
||||||
|
if (y === 0) {
|
||||||
|
y0 -= paddingY;
|
||||||
|
h += paddingY;
|
||||||
|
} else if (y === content.height - 1) {
|
||||||
|
h += paddingY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw background
|
||||||
|
cx.fillStyle = toColor(content.palette[backgroundColor]);
|
||||||
|
cx.fillRect(x0, y0, w, h);
|
||||||
|
|
||||||
|
// compose foreground
|
||||||
|
ccx.globalCompositeOperation = "source-over";
|
||||||
|
ccx.clearRect(0, 0, charWidth, charHeight);
|
||||||
|
|
||||||
|
ccx.fillStyle = toColor(content.palette[textColor]);
|
||||||
|
ccx.fillRect(0, 0, charWidth, charHeight);
|
||||||
|
ccx.globalCompositeOperation = "destination-in";
|
||||||
|
ccx.drawImage(font[charCode], 0, 0);
|
||||||
|
|
||||||
|
// draw foreground
|
||||||
|
cx.drawImage(composeCanvas, offsetX, offsetY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redraw(content: ScreenContent) {
|
||||||
|
if (!canvas) return;
|
||||||
|
if (!font) return;
|
||||||
|
|
||||||
|
const cx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
cx.fillStyle = "#ff6600";
|
||||||
|
cx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = content.width * charWidth + 2 * paddingX;
|
||||||
|
canvas.height = content.height * charHeight + 2 * paddingY;
|
||||||
|
|
||||||
|
cx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
for (let y = 0; y < content.height; ++y) {
|
||||||
|
const line = content.text[y];
|
||||||
|
const fgLine = content.fg_color[y];
|
||||||
|
const bgLine = content.bg_color[y];
|
||||||
|
|
||||||
|
for (let x = 0; x < content.width; ++x) {
|
||||||
|
drawChar(
|
||||||
|
line.charCodeAt(x),
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
parseInt(fgLine.charAt(x), 16),
|
||||||
|
parseInt(bgLine.charAt(x), 16)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: redraw(content);
|
||||||
|
|
||||||
|
const blinkInt = setInterval(() => {
|
||||||
|
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
font = await loadFont(termFont);
|
||||||
|
composeCanvas = document.createElement("canvas");
|
||||||
|
composeCanvas.width = charWidth;
|
||||||
|
composeCanvas.height = charHeight;
|
||||||
|
redraw(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
composeCanvas = null;
|
||||||
|
clearInterval(blinkInt);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas bind:this={canvas} />
|
65
frontend/src/pages/monitoring/Viewer.svelte
Normal file
65
frontend/src/pages/monitoring/Viewer.svelte
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let uuid: string;
|
||||||
|
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import Screen from "./Screen.svelte";
|
||||||
|
|
||||||
|
const wsUrl = new URL(`/ipmi/browser/${uuid}/ws`, window.location.href);
|
||||||
|
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
|
||||||
|
|
||||||
|
const dummyData = {
|
||||||
|
x: -1,
|
||||||
|
y: -1,
|
||||||
|
width: 13,
|
||||||
|
height: 5,
|
||||||
|
blink: false,
|
||||||
|
fg: 0,
|
||||||
|
text: [
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
" [NO SIGNAL] ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
],
|
||||||
|
fg_color: [
|
||||||
|
"0000000000000",
|
||||||
|
"0000000000000",
|
||||||
|
"0800000000080",
|
||||||
|
"0000000000000",
|
||||||
|
"0000000000000",
|
||||||
|
],
|
||||||
|
bg_color: [
|
||||||
|
"7777777777777",
|
||||||
|
"7777777777777",
|
||||||
|
"7777777777777",
|
||||||
|
"7777777777777",
|
||||||
|
"7777777777777",
|
||||||
|
],
|
||||||
|
palette: [
|
||||||
|
15790320, 15905331, 15040472, 10072818, 14605932, 8375321, 15905484,
|
||||||
|
5000268, 10066329, 5020082, 11691749, 3368652, 8349260, 5744206,
|
||||||
|
13388876, 1118481,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let socket: WebSocket | null = null;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
socket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
data = JSON.parse(event.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
console.log("onDestroy");
|
||||||
|
if (socket !== null) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Screen content={data || dummyData} />
|
||||||
|
<pre>{JSON.stringify(data, null, 2)}</pre>
|
25
frontend/src/pages/monitoring/font.ts
Normal file
25
frontend/src/pages/monitoring/font.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export const charWidth = 6, charHeight = 9;
|
||||||
|
|
||||||
|
export async function loadFont(src: RequestInfo | URL): Promise<ImageBitmap[]> {
|
||||||
|
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);
|
||||||
|
}
|
12
frontend/src/pages/monitoring/proto.ts
Normal file
12
frontend/src/pages/monitoring/proto.ts
Normal file
@ -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[]
|
||||||
|
}
|
BIN
frontend/src/pages/monitoring/term_font.png
Normal file
BIN
frontend/src/pages/monitoring/term_font.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
21
frontend/src/pages/playground/Bar.svelte
Normal file
21
frontend/src/pages/playground/Bar.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
|
import Canvas from "./Canvas.svelte";
|
||||||
|
|
||||||
|
let colors = ["#ff6600", "#00ff66", "#6600ff"];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
$: color = colors[i];
|
||||||
|
|
||||||
|
let int = setInterval(() => {
|
||||||
|
i = (i + 1) % colors.length;
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(int);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<Canvas {color} />
|
||||||
|
</section>
|
24
frontend/src/pages/playground/Canvas.svelte
Normal file
24
frontend/src/pages/playground/Canvas.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
export let color: string;
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
let redraws = 0;
|
||||||
|
|
||||||
|
function redraw(color) {
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
redraws += 1;
|
||||||
|
let cx = canvas.getContext("2d");
|
||||||
|
cx.fillStyle = color;
|
||||||
|
cx.fillRect(0, 0, cx.canvas.width, cx.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: redraw(color);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas bind:this={canvas} />
|
||||||
|
<pre>color: {color}
|
||||||
|
redraws: {redraws}</pre>
|
@ -2,7 +2,7 @@ local json = require("json")
|
|||||||
local fb = require("framebuffer")
|
local fb = require("framebuffer")
|
||||||
local ringbuffer = require("ringbuffer")
|
local ringbuffer = require("ringbuffer")
|
||||||
local UUID <const> = "8b9faf9f-9470-4a50-b405-0af5f0152550"
|
local UUID <const> = "8b9faf9f-9470-4a50-b405-0af5f0152550"
|
||||||
local ENDPOINT <const> = "ws://localhost:8000/monitoring/computer/" .. UUID .. "/ws"
|
local ENDPOINT <const> = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws"
|
||||||
|
|
||||||
print("[MAIN] Init")
|
print("[MAIN] Init")
|
||||||
|
|
||||||
|
96
server/dummy-client.py
Normal file
96
server/dummy-client.py
Normal file
@ -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")
|
@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
@ -6,14 +7,20 @@ from .settings import settings
|
|||||||
from .user import user_auth
|
from .user import user_auth
|
||||||
from .map_tiles import map_tiles, map_meta
|
from .map_tiles import map_tiles, map_meta
|
||||||
from .templates import j2env
|
from .templates import j2env
|
||||||
from .monitoring import monitoring
|
from .monitoring import monitoring, ws_manager
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
app.mount("/user/", user_auth)
|
app.mount("/user/", user_auth)
|
||||||
app.mount("/map/", map_meta)
|
app.mount("/map/", map_meta)
|
||||||
app.mount("/tiles/", map_tiles)
|
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()
|
frontend = FastAPI()
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
monitoring = FastAPI()
|
monitoring = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
class ScreenContent(BaseModel):
|
class ScreenContent(BaseModel):
|
||||||
x: int
|
x: int
|
||||||
y: int
|
y: int
|
||||||
@ -15,24 +18,86 @@ class ScreenContent(BaseModel):
|
|||||||
text: list[str]
|
text: list[str]
|
||||||
fg_color: list[str]
|
fg_color: list[str]
|
||||||
bg_color: list[str]
|
bg_color: list[str]
|
||||||
|
palette: list[int]
|
||||||
|
|
||||||
class Ping(BaseModel):
|
|
||||||
screen: ScreenContent
|
|
||||||
|
|
||||||
@monitoring.post("/ping")
|
class Update(BaseModel):
|
||||||
async def ping(request: Request, data: Ping):
|
screen: ScreenContent | None
|
||||||
print("[PING]")
|
|
||||||
for line in data.screen.text:
|
|
||||||
print(line)
|
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")
|
@monitoring.websocket("/computer/{uuid}/ws")
|
||||||
async def computer_ws(socket: WebSocket, uuid: UUID):
|
async def computer_ws(socket: WebSocket, uuid: UUID):
|
||||||
await socket.accept()
|
await socket.accept()
|
||||||
print(f"[WS] Computer {uuid} connected")
|
await ws_manager.on_computer_connect(socket, uuid)
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data = await socket.receive_json()
|
@monitoring.websocket("/browser/{uuid}/ws")
|
||||||
#print(f"[WS] rx {json.dumps(data)}")
|
async def browser_ws(socket: WebSocket, uuid: UUID):
|
||||||
except WebSocketDisconnect:
|
await socket.accept()
|
||||||
break
|
await ws_manager.on_browser_connect(socket, uuid)
|
||||||
print(f"[WS] Computer {uuid} disconnected")
|
|
||||||
|
Loading…
Reference in New Issue
Block a user