Merge branch 'feature/monitoring'
This commit is contained in:
commit
8886b23434
1
db/.gitignore
vendored
Normal file
1
db/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
secret.env
|
19
db/docker-compose.yml
Normal file
19
db/docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Use root/example as user/password credentials
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
env_file:
|
||||||
|
- secret.env
|
||||||
|
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8081:8081
|
||||||
|
env_file:
|
||||||
|
- secret.env
|
5
db/secret.env.example
Normal file
5
db/secret.env.example
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MONGO_INITDB_ROOT_USERNAME="user"
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD="pass"
|
||||||
|
ME_CONFIG_MONGODB_ADMINUSERNAME="user"
|
||||||
|
ME_CONFIG_MONGODB_ADMINPASSWORD="pass"
|
||||||
|
ME_CONFIG_MONGODB_URL="mongodb://user:pass@mongo:27017/"
|
@ -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>
|
1
lua/.gitignore
vendored
Normal file
1
lua/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
out
|
20
lua/cc.d.tl
Normal file
20
lua/cc.d.tl
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
global sleep: function(time: number)
|
||||||
|
|
||||||
|
global write: function(text: string): number
|
||||||
|
--global print: function(...: any): number
|
||||||
|
global printError: function(...: any)
|
||||||
|
|
||||||
|
--global type ReadCompletionFunction = function(partial: string): { string } | nil
|
||||||
|
--global read: function(replaceChar: string | nil, history: table | nil, completeFn: ReadCompletionFunction | nil, default: string | nil): string
|
||||||
|
|
||||||
|
global _HOST: string
|
||||||
|
global _CC_DEFAULT_SETTINGS: string
|
||||||
|
|
||||||
|
require("types/colors")
|
||||||
|
require("types/term")
|
||||||
|
require("types/parallel")
|
||||||
|
require("types/http")
|
||||||
|
require("types/shell")
|
||||||
|
require("types/window")
|
||||||
|
require("types/os")
|
||||||
|
|
139
lua/framebuffer.tl
Normal file
139
lua/framebuffer.tl
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
local record ScreenContent
|
||||||
|
x: integer
|
||||||
|
y: integer
|
||||||
|
width: integer
|
||||||
|
height: integer
|
||||||
|
blink: boolean
|
||||||
|
fg: integer
|
||||||
|
text: {string}
|
||||||
|
fg_color: {string}
|
||||||
|
bg_color: {string}
|
||||||
|
palette: {integer}
|
||||||
|
end
|
||||||
|
|
||||||
|
local record Buffer
|
||||||
|
target: term.Redirect
|
||||||
|
serialize: function(): ScreenContent
|
||||||
|
is_dirty: function(): boolean
|
||||||
|
clear_dirty: function()
|
||||||
|
end
|
||||||
|
|
||||||
|
local COLOR_LOOKUP <const> : {number: integer} = {
|
||||||
|
[colors.white] = 0x0,
|
||||||
|
[colors.orange] = 0x1,
|
||||||
|
[colors.magenta] = 0x2,
|
||||||
|
[colors.lightBlue] = 0x3,
|
||||||
|
[colors.yellow] = 0x4,
|
||||||
|
[colors.lime] = 0x5,
|
||||||
|
[colors.pink] = 0x6,
|
||||||
|
[colors.gray] = 0x7,
|
||||||
|
[colors.lightGray] = 0x8,
|
||||||
|
[colors.cyan] = 0x9,
|
||||||
|
[colors.purple] = 0xA,
|
||||||
|
[colors.blue] = 0xB,
|
||||||
|
[colors.brown] = 0xC,
|
||||||
|
[colors.green] = 0xD,
|
||||||
|
[colors.red] = 0xE,
|
||||||
|
[colors.black] = 0xF,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function wrap(parent: term.Redirect): Buffer
|
||||||
|
local x, y = parent.getCursorPos()
|
||||||
|
local width, height = parent.getSize()
|
||||||
|
local blink = parent.getCursorBlink()
|
||||||
|
local fg = COLOR_LOOKUP[parent.getTextColor()]
|
||||||
|
local palette: {number} = {}
|
||||||
|
for c = 0, 15 do
|
||||||
|
palette[c+1] = colors.packRGB(parent.getPaletteColor(2^c))
|
||||||
|
end
|
||||||
|
local dirty: boolean = false
|
||||||
|
|
||||||
|
local win = window.create(parent, 1, 1, width, height)
|
||||||
|
|
||||||
|
local overrides: table = {}
|
||||||
|
|
||||||
|
overrides.setCursorPos = function(new_x: integer, new_y: integer)
|
||||||
|
win.setCursorPos(new_x, new_y)
|
||||||
|
x = new_x
|
||||||
|
y = new_y
|
||||||
|
end
|
||||||
|
|
||||||
|
overrides.setCursorBlink = function(new_blink: boolean)
|
||||||
|
win.setCursorBlink(new_blink)
|
||||||
|
blink = new_blink
|
||||||
|
end
|
||||||
|
|
||||||
|
overrides.setTextColor = function(new_color: number)
|
||||||
|
local r = { pcall(win.setTextColor, new_color) }
|
||||||
|
if not r[1] then
|
||||||
|
error((r[2] as string):sub(8))
|
||||||
|
end
|
||||||
|
fg = COLOR_LOOKUP[new_color]
|
||||||
|
end
|
||||||
|
overrides.setTextColour = overrides.setTextColor
|
||||||
|
|
||||||
|
overrides.setPaletteColor = function(color: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
local r = { pcall(win.setPaletteColor, color, r_rgb, g, b) }
|
||||||
|
if not r[1] then
|
||||||
|
error((r[2] as string):sub(8))
|
||||||
|
end
|
||||||
|
local index = COLOR_LOOKUP[color]
|
||||||
|
if g == nil then
|
||||||
|
palette[1 + index] = r_rgb
|
||||||
|
else
|
||||||
|
palette[1 + index] = colors.packRGB(r_rgb, g, b)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
overrides.setPaletteColour = overrides.setPaletteColor
|
||||||
|
|
||||||
|
local target = setmetatable(overrides, {
|
||||||
|
__index = function(_: table, k: any): any
|
||||||
|
dirty = true
|
||||||
|
return (win as table)[k]
|
||||||
|
end
|
||||||
|
}) as term.Redirect
|
||||||
|
|
||||||
|
target.setTextColor(colors.white)
|
||||||
|
target.setBackgroundColor(colors.black)
|
||||||
|
target.setCursorPos(1,1)
|
||||||
|
target.clear()
|
||||||
|
|
||||||
|
local buffer: Buffer = {
|
||||||
|
target = target
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.serialize = function(): ScreenContent
|
||||||
|
local text: {string} = {}
|
||||||
|
local fg_color: {string} = {}
|
||||||
|
local bg_color: {string} = {}
|
||||||
|
for i = 1, height do
|
||||||
|
local t, f, b = win.getLine(i)
|
||||||
|
table.insert(text, t)
|
||||||
|
table.insert(fg_color, f)
|
||||||
|
table.insert(bg_color, b)
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
blink = blink,
|
||||||
|
fg = fg,
|
||||||
|
text = text,
|
||||||
|
fg_color = fg_color,
|
||||||
|
bg_color = bg_color,
|
||||||
|
palette = palette
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer.is_dirty = function(): boolean
|
||||||
|
return dirty
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer.clear_dirty = function() dirty = false end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
return { wrap = wrap, Buffer = Buffer, ScreenContent = ScreenContent }
|
6
lua/json.d.tl
Normal file
6
lua/json.d.tl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
local record Json
|
||||||
|
encode: function(value: any): string
|
||||||
|
decode: function(str: string): any
|
||||||
|
end
|
||||||
|
|
||||||
|
return Json
|
388
lua/json.lua
Normal file
388
lua/json.lua
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
--
|
||||||
|
-- json.lua
|
||||||
|
--
|
||||||
|
-- Copyright (c) 2020 rxi
|
||||||
|
--
|
||||||
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
-- this software and associated documentation files (the "Software"), to deal in
|
||||||
|
-- the Software without restriction, including without limitation the rights to
|
||||||
|
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
-- so, subject to the following conditions:
|
||||||
|
--
|
||||||
|
-- The above copyright notice and this permission notice shall be included in all
|
||||||
|
-- copies or substantial portions of the Software.
|
||||||
|
--
|
||||||
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
-- SOFTWARE.
|
||||||
|
--
|
||||||
|
|
||||||
|
local json = { _version = "0.1.2" }
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Encode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local encode
|
||||||
|
|
||||||
|
local escape_char_map = {
|
||||||
|
[ "\\" ] = "\\",
|
||||||
|
[ "\"" ] = "\"",
|
||||||
|
[ "\b" ] = "b",
|
||||||
|
[ "\f" ] = "f",
|
||||||
|
[ "\n" ] = "n",
|
||||||
|
[ "\r" ] = "r",
|
||||||
|
[ "\t" ] = "t",
|
||||||
|
}
|
||||||
|
|
||||||
|
local escape_char_map_inv = { [ "/" ] = "/" }
|
||||||
|
for k, v in pairs(escape_char_map) do
|
||||||
|
escape_char_map_inv[v] = k
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function escape_char(c)
|
||||||
|
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_nil(val)
|
||||||
|
return "null"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_table(val, stack)
|
||||||
|
local res = {}
|
||||||
|
stack = stack or {}
|
||||||
|
|
||||||
|
-- Circular reference?
|
||||||
|
if stack[val] then error("circular reference") end
|
||||||
|
|
||||||
|
stack[val] = true
|
||||||
|
|
||||||
|
if rawget(val, 1) ~= nil or next(val) == nil then
|
||||||
|
-- Treat as array -- check keys are valid and it is not sparse
|
||||||
|
local n = 0
|
||||||
|
for k in pairs(val) do
|
||||||
|
if type(k) ~= "number" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
if n ~= #val then
|
||||||
|
error("invalid table: sparse array")
|
||||||
|
end
|
||||||
|
-- Encode
|
||||||
|
for i, v in ipairs(val) do
|
||||||
|
table.insert(res, encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "[" .. table.concat(res, ",") .. "]"
|
||||||
|
|
||||||
|
else
|
||||||
|
-- Treat as an object
|
||||||
|
for k, v in pairs(val) do
|
||||||
|
if type(k) ~= "string" then
|
||||||
|
error("invalid table: mixed or invalid key types")
|
||||||
|
end
|
||||||
|
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||||
|
end
|
||||||
|
stack[val] = nil
|
||||||
|
return "{" .. table.concat(res, ",") .. "}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_string(val)
|
||||||
|
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function encode_number(val)
|
||||||
|
-- Check for NaN, -inf and inf
|
||||||
|
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||||
|
error("unexpected number value '" .. tostring(val) .. "'")
|
||||||
|
end
|
||||||
|
return string.format("%.14g", val)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local type_func_map = {
|
||||||
|
[ "nil" ] = encode_nil,
|
||||||
|
[ "table" ] = encode_table,
|
||||||
|
[ "string" ] = encode_string,
|
||||||
|
[ "number" ] = encode_number,
|
||||||
|
[ "boolean" ] = tostring,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
encode = function(val, stack)
|
||||||
|
local t = type(val)
|
||||||
|
local f = type_func_map[t]
|
||||||
|
if f then
|
||||||
|
return f(val, stack)
|
||||||
|
end
|
||||||
|
error("unexpected type '" .. t .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.encode(val)
|
||||||
|
return ( encode(val) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Decode
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local parse
|
||||||
|
|
||||||
|
local function create_set(...)
|
||||||
|
local res = {}
|
||||||
|
for i = 1, select("#", ...) do
|
||||||
|
res[ select(i, ...) ] = true
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||||
|
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||||
|
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||||
|
local literals = create_set("true", "false", "null")
|
||||||
|
|
||||||
|
local literal_map = {
|
||||||
|
[ "true" ] = true,
|
||||||
|
[ "false" ] = false,
|
||||||
|
[ "null" ] = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function next_char(str, idx, set, negate)
|
||||||
|
for i = idx, #str do
|
||||||
|
if set[str:sub(i, i)] ~= negate then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return #str + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function decode_error(str, idx, msg)
|
||||||
|
local line_count = 1
|
||||||
|
local col_count = 1
|
||||||
|
for i = 1, idx - 1 do
|
||||||
|
col_count = col_count + 1
|
||||||
|
if str:sub(i, i) == "\n" then
|
||||||
|
line_count = line_count + 1
|
||||||
|
col_count = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function codepoint_to_utf8(n)
|
||||||
|
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||||
|
local f = math.floor
|
||||||
|
if n <= 0x7f then
|
||||||
|
return string.char(n)
|
||||||
|
elseif n <= 0x7ff then
|
||||||
|
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||||
|
elseif n <= 0xffff then
|
||||||
|
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
elseif n <= 0x10ffff then
|
||||||
|
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||||
|
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||||
|
end
|
||||||
|
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_unicode_escape(s)
|
||||||
|
local n1 = tonumber( s:sub(1, 4), 16 )
|
||||||
|
local n2 = tonumber( s:sub(7, 10), 16 )
|
||||||
|
-- Surrogate pair?
|
||||||
|
if n2 then
|
||||||
|
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||||
|
else
|
||||||
|
return codepoint_to_utf8(n1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_string(str, i)
|
||||||
|
local res = ""
|
||||||
|
local j = i + 1
|
||||||
|
local k = j
|
||||||
|
|
||||||
|
while j <= #str do
|
||||||
|
local x = str:byte(j)
|
||||||
|
|
||||||
|
if x < 32 then
|
||||||
|
decode_error(str, j, "control character in string")
|
||||||
|
|
||||||
|
elseif x == 92 then -- `\`: Escape
|
||||||
|
res = res .. str:sub(k, j - 1)
|
||||||
|
j = j + 1
|
||||||
|
local c = str:sub(j, j)
|
||||||
|
if c == "u" then
|
||||||
|
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
|
||||||
|
or str:match("^%x%x%x%x", j + 1)
|
||||||
|
or decode_error(str, j - 1, "invalid unicode escape in string")
|
||||||
|
res = res .. parse_unicode_escape(hex)
|
||||||
|
j = j + #hex
|
||||||
|
else
|
||||||
|
if not escape_chars[c] then
|
||||||
|
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
|
||||||
|
end
|
||||||
|
res = res .. escape_char_map_inv[c]
|
||||||
|
end
|
||||||
|
k = j + 1
|
||||||
|
|
||||||
|
elseif x == 34 then -- `"`: End of string
|
||||||
|
res = res .. str:sub(k, j - 1)
|
||||||
|
return res, j + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
j = j + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
decode_error(str, i, "expected closing quote for string")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_number(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local s = str:sub(i, x - 1)
|
||||||
|
local n = tonumber(s)
|
||||||
|
if not n then
|
||||||
|
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||||
|
end
|
||||||
|
return n, x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_literal(str, i)
|
||||||
|
local x = next_char(str, i, delim_chars)
|
||||||
|
local word = str:sub(i, x - 1)
|
||||||
|
if not literals[word] then
|
||||||
|
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||||
|
end
|
||||||
|
return literal_map[word], x
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_array(str, i)
|
||||||
|
local res = {}
|
||||||
|
local n = 1
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local x
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of array?
|
||||||
|
if str:sub(i, i) == "]" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read token
|
||||||
|
x, i = parse(str, i)
|
||||||
|
res[n] = x
|
||||||
|
n = n + 1
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "]" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local function parse_object(str, i)
|
||||||
|
local res = {}
|
||||||
|
i = i + 1
|
||||||
|
while 1 do
|
||||||
|
local key, val
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
-- Empty / end of object?
|
||||||
|
if str:sub(i, i) == "}" then
|
||||||
|
i = i + 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
-- Read key
|
||||||
|
if str:sub(i, i) ~= '"' then
|
||||||
|
decode_error(str, i, "expected string for key")
|
||||||
|
end
|
||||||
|
key, i = parse(str, i)
|
||||||
|
-- Read ':' delimiter
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
if str:sub(i, i) ~= ":" then
|
||||||
|
decode_error(str, i, "expected ':' after key")
|
||||||
|
end
|
||||||
|
i = next_char(str, i + 1, space_chars, true)
|
||||||
|
-- Read value
|
||||||
|
val, i = parse(str, i)
|
||||||
|
-- Set
|
||||||
|
res[key] = val
|
||||||
|
-- Next token
|
||||||
|
i = next_char(str, i, space_chars, true)
|
||||||
|
local chr = str:sub(i, i)
|
||||||
|
i = i + 1
|
||||||
|
if chr == "}" then break end
|
||||||
|
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||||
|
end
|
||||||
|
return res, i
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local char_func_map = {
|
||||||
|
[ '"' ] = parse_string,
|
||||||
|
[ "0" ] = parse_number,
|
||||||
|
[ "1" ] = parse_number,
|
||||||
|
[ "2" ] = parse_number,
|
||||||
|
[ "3" ] = parse_number,
|
||||||
|
[ "4" ] = parse_number,
|
||||||
|
[ "5" ] = parse_number,
|
||||||
|
[ "6" ] = parse_number,
|
||||||
|
[ "7" ] = parse_number,
|
||||||
|
[ "8" ] = parse_number,
|
||||||
|
[ "9" ] = parse_number,
|
||||||
|
[ "-" ] = parse_number,
|
||||||
|
[ "t" ] = parse_literal,
|
||||||
|
[ "f" ] = parse_literal,
|
||||||
|
[ "n" ] = parse_literal,
|
||||||
|
[ "[" ] = parse_array,
|
||||||
|
[ "{" ] = parse_object,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parse = function(str, idx)
|
||||||
|
local chr = str:sub(idx, idx)
|
||||||
|
local f = char_func_map[chr]
|
||||||
|
if f then
|
||||||
|
return f(str, idx)
|
||||||
|
end
|
||||||
|
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function json.decode(str)
|
||||||
|
if type(str) ~= "string" then
|
||||||
|
error("expected argument of type string, got " .. type(str))
|
||||||
|
end
|
||||||
|
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
||||||
|
idx = next_char(str, idx, space_chars, true)
|
||||||
|
if idx <= #str then
|
||||||
|
decode_error(str, idx, "trailing garbage")
|
||||||
|
end
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
return json
|
30
lua/justfile
Normal file
30
lua/justfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
lua_files := `find . -type f -name "*.lua" ! -path './out/*' ! -name tlconfig.lua -printf "%p "`
|
||||||
|
teal_files := `find . -type f -name "*.tl" ! -name '*.d.tl' -printf "%p "`
|
||||||
|
|
||||||
|
build:
|
||||||
|
mkdir -p out
|
||||||
|
for file in {{lua_files}}; do \
|
||||||
|
cp $file out; \
|
||||||
|
done
|
||||||
|
for file in {{teal_files}}; do \
|
||||||
|
tl gen $file; \
|
||||||
|
mv ${file%.tl}.lua out; \
|
||||||
|
done
|
||||||
|
|
||||||
|
alias b := build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -r out
|
||||||
|
|
||||||
|
alias c := clean
|
||||||
|
|
||||||
|
watch:
|
||||||
|
while sleep 0.1; do \
|
||||||
|
find . -type f ! -path './out/*' | entr -d just build; \
|
||||||
|
[ $? -eq 0 ] && exit 0; \
|
||||||
|
done
|
||||||
|
|
||||||
|
alias w := watch
|
164
lua/main.tl
Normal file
164
lua/main.tl
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
local json = require("json")
|
||||||
|
local Framebuffer = require("framebuffer")
|
||||||
|
local Ringbuffer = require("ringbuffer")
|
||||||
|
local Socket = require("socket")
|
||||||
|
local UUID <const> = "8b9faf9f-9470-4a50-b405-0af5f0152550"
|
||||||
|
local ENDPOINT <const> = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws"
|
||||||
|
|
||||||
|
print("[MAIN] Init")
|
||||||
|
|
||||||
|
local socket = Socket.new(ENDPOINT)
|
||||||
|
|
||||||
|
-- Set up framebuffer capture and statusline
|
||||||
|
|
||||||
|
print("[MAIN] Setup framebuffer")
|
||||||
|
|
||||||
|
local prev_term = term.current()
|
||||||
|
local orig_native = term.native
|
||||||
|
local buffer = Framebuffer.wrap(orig_native())
|
||||||
|
term.native = function(): term.Redirect
|
||||||
|
return buffer.target
|
||||||
|
end
|
||||||
|
|
||||||
|
local width, height = term.getSize()
|
||||||
|
local top_line = window.create(buffer.target, 1, 1, width, 1)
|
||||||
|
local main_view = window.create(buffer.target, 1, 2, width, height - 1)
|
||||||
|
term.redirect(main_view as term.Redirect)
|
||||||
|
|
||||||
|
local function set_bar(text: string, fg: string | nil, bg: string | nil)
|
||||||
|
fg = fg or ("9"):rep(text:len())
|
||||||
|
bg = bg or ("f"):rep(text:len())
|
||||||
|
top_line.clear()
|
||||||
|
top_line.setCursorPos(1,1)
|
||||||
|
top_line.blit(text, fg, bg)
|
||||||
|
main_view.restoreCursor()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create tasks
|
||||||
|
|
||||||
|
local bar_codes: { Socket.State: {string} } = {
|
||||||
|
["reset"] = {"[WS] RST", "78870111"},
|
||||||
|
["error"] = {"[WS] ERR", "78870EEE"},
|
||||||
|
["ok"] = {"[WS] OK\x03", "78870DD5"},
|
||||||
|
["viewer_connected"] = {"[WS] CON", "78870999"},
|
||||||
|
}
|
||||||
|
|
||||||
|
socket:on_state_change(function(new_state: Socket.State)
|
||||||
|
set_bar(table.unpack(bar_codes[new_state]))
|
||||||
|
end)
|
||||||
|
|
||||||
|
local ws_task = coroutine.create(function()
|
||||||
|
while true do
|
||||||
|
if socket:is_bad_state() then
|
||||||
|
socket:reconnect()
|
||||||
|
end
|
||||||
|
sleep(1)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local report_task = coroutine.create(function()
|
||||||
|
local last_report = -1.0
|
||||||
|
while true do
|
||||||
|
local now = os.clock()
|
||||||
|
local interval = (socket.state == "viewer_connected") and 0.05 or 1
|
||||||
|
if now - last_report >= interval then
|
||||||
|
local message = json.encode({
|
||||||
|
screen = buffer.serialize()
|
||||||
|
})
|
||||||
|
socket:send(message)
|
||||||
|
last_report = now
|
||||||
|
end
|
||||||
|
sleep(0) -- until next gametick
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- basically parallel.waitForAny
|
||||||
|
|
||||||
|
local record Task
|
||||||
|
coro: thread
|
||||||
|
filter: string | nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local background_tasks: {Task} = {
|
||||||
|
{coro = ws_task},
|
||||||
|
{coro = report_task},
|
||||||
|
}
|
||||||
|
|
||||||
|
local shell_task: Task = {
|
||||||
|
coro = coroutine.create(function() shell.run("shell") end)
|
||||||
|
}
|
||||||
|
|
||||||
|
local function handle_event(e: table, pid: integer)
|
||||||
|
if e[1] == "terminate" then return end
|
||||||
|
|
||||||
|
local task = background_tasks[pid]
|
||||||
|
if task.filter == nil or task.filter == e[1] then
|
||||||
|
local ok, param = coroutine.resume(task.coro, table.unpack(e as {any}))
|
||||||
|
if not ok then
|
||||||
|
term.native = orig_native
|
||||||
|
term.redirect(term.native())
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1,1)
|
||||||
|
print(("OMEGABIG OOF @ PID %d"):format(pid))
|
||||||
|
error(param, 0)
|
||||||
|
else
|
||||||
|
task.filter = param as string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local event_queue: Ringbuffer.Ringbuffer<table> = Ringbuffer.new(64)
|
||||||
|
event_queue:push({n = 0})
|
||||||
|
|
||||||
|
local shell_deaths: {any} = {}
|
||||||
|
|
||||||
|
local shell_running = true
|
||||||
|
while shell_running do
|
||||||
|
local e: table
|
||||||
|
|
||||||
|
if not event_queue:is_empty() then
|
||||||
|
e = event_queue:pop() as table
|
||||||
|
else
|
||||||
|
e = table.pack(os.pullEventRaw())
|
||||||
|
end
|
||||||
|
|
||||||
|
if e[1] == "websocket_message" and e[2] == ENDPOINT then
|
||||||
|
local payload = json.decode(e[3] as string) as table
|
||||||
|
if payload["type"] == "push_event" then
|
||||||
|
event_queue:push(payload["event"] as table)
|
||||||
|
elseif payload["type"] == "viewer_connect" then
|
||||||
|
socket:signal_viewer_connect(true)
|
||||||
|
elseif payload["type"] == "viewer_disconnect" then
|
||||||
|
socket:signal_viewer_connect(false)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for pid = 1, #background_tasks do
|
||||||
|
handle_event(e, pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
if shell_task.filter == nil or shell_task.filter == e[1] or e[1] == "terminate" then
|
||||||
|
local ok, param = coroutine.resume(shell_task.coro, table.unpack(e as {any}))
|
||||||
|
if not ok then
|
||||||
|
-- shell died i guess?
|
||||||
|
table.insert(shell_deaths, param)
|
||||||
|
else
|
||||||
|
shell_task.filter = param as string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if coroutine.status(shell_task.coro) == "dead" then
|
||||||
|
shell_running = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
socket:close()
|
||||||
|
|
||||||
|
term.native = orig_native
|
||||||
|
term.redirect(prev_term)
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1,1)
|
||||||
|
|
||||||
|
for i = 1, #shell_deaths do
|
||||||
|
print(shell_deaths[i])
|
||||||
|
end
|
42
lua/ringbuffer.tl
Normal file
42
lua/ringbuffer.tl
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
local record Ringbuffer<T>
|
||||||
|
{T}
|
||||||
|
push: function(self: Ringbuffer<T>, el: T): boolean
|
||||||
|
pop: function(self: Ringbuffer<T>): T | nil
|
||||||
|
is_empty: function(self: Ringbuffer<T>): boolean
|
||||||
|
|
||||||
|
head: integer
|
||||||
|
n: integer
|
||||||
|
size: integer
|
||||||
|
end
|
||||||
|
|
||||||
|
local impl: table = {}
|
||||||
|
|
||||||
|
impl.push = function<T>(self: Ringbuffer<T>, el: T): boolean
|
||||||
|
if self.n == self.size then return false end
|
||||||
|
-- items are at head + 0, head + 1, ..., head + (n-1)
|
||||||
|
local tail = (self.head + self.n) % self.size
|
||||||
|
self[1 + tail] = el
|
||||||
|
self.n = self.n + 1
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.pop = function<T>(self: Ringbuffer<T>): T | nil
|
||||||
|
if self.n == 0 then return nil end
|
||||||
|
local res = self[1 + self.head]
|
||||||
|
self.head = (self.head + 1) % self.size
|
||||||
|
self.n = self.n - 1
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.is_empty = function<T>(self: Ringbuffer<T>): boolean
|
||||||
|
return self.n == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function new<T>(size: integer): Ringbuffer<T>
|
||||||
|
return setmetatable({ head = 0, n = 0, size = size }, { __index = impl })
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
new = new,
|
||||||
|
Ringbuffer = Ringbuffer
|
||||||
|
}
|
92
lua/socket.tl
Normal file
92
lua/socket.tl
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
local enum State
|
||||||
|
"reset"
|
||||||
|
"error"
|
||||||
|
"ok"
|
||||||
|
"viewer_connected"
|
||||||
|
end
|
||||||
|
|
||||||
|
local BAD_STATES <const> : {State: boolean} = {
|
||||||
|
["reset"] = true,
|
||||||
|
["error"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local type StateCallback = function(new_state: State)
|
||||||
|
|
||||||
|
local record Socket
|
||||||
|
state: State
|
||||||
|
is_bad_state: function(self: Socket): boolean
|
||||||
|
_set_state: function(self: Socket, state: State)
|
||||||
|
on_state_change: function(self: Socket, cb: StateCallback)
|
||||||
|
send: function(self: Socket, message: string)
|
||||||
|
reconnect: function(self: Socket)
|
||||||
|
close: function(self: Socket)
|
||||||
|
signal_viewer_connect: function(self: Socket, connected: boolean)
|
||||||
|
_endpoint: string
|
||||||
|
_callback: StateCallback
|
||||||
|
_ws: http.Websocket
|
||||||
|
end
|
||||||
|
|
||||||
|
local impl: table = {}
|
||||||
|
|
||||||
|
impl.is_bad_state = function(self: Socket): boolean
|
||||||
|
return BAD_STATES[self.state] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
impl._set_state = function(self: Socket, state: State)
|
||||||
|
self.state = state
|
||||||
|
self._callback(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.on_state_change = function(self: Socket, cb: StateCallback)
|
||||||
|
self._callback = cb
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.send = function(self: Socket, message: string)
|
||||||
|
-- "message" needs to be valid JSON
|
||||||
|
-- otherwise the server will not accept it
|
||||||
|
|
||||||
|
if self:is_bad_state() then return end
|
||||||
|
|
||||||
|
local r = { pcall(self._ws.send, message) }
|
||||||
|
|
||||||
|
if r[1] == false then
|
||||||
|
if (r[2] as string):sub(-11) == "closed file" then
|
||||||
|
self:_set_state("reset")
|
||||||
|
elseif (r[2] as string):sub(-9) == "too large" then
|
||||||
|
-- TODO handle
|
||||||
|
-- the connection stays open though
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.reconnect = function(self: Socket)
|
||||||
|
local r = http.websocket(self._endpoint)
|
||||||
|
if r ~= false then
|
||||||
|
self._ws = r as http.Websocket
|
||||||
|
self:_set_state("ok")
|
||||||
|
else
|
||||||
|
self:_set_state("error")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.close = function(self: Socket)
|
||||||
|
if self:is_bad_state() then return end
|
||||||
|
self._ws.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
impl.signal_viewer_connect = function(self: Socket, connected: boolean)
|
||||||
|
if self:is_bad_state() then return end --how?
|
||||||
|
local new_state: State = connected and "viewer_connected" or "ok"
|
||||||
|
self:_set_state(new_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function new(endpoint: string): Socket
|
||||||
|
return setmetatable({
|
||||||
|
state = "reset",
|
||||||
|
_endpoint = endpoint,
|
||||||
|
_callback = function(_: State) end,
|
||||||
|
_ws = nil,
|
||||||
|
}, { __index = impl })
|
||||||
|
end
|
||||||
|
|
||||||
|
return { new = new, State = State, StateCallback = StateCallback, Socket = Socket }
|
4
lua/tlconfig.lua
Normal file
4
lua/tlconfig.lua
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
return {
|
||||||
|
global_env_def = "cc",
|
||||||
|
gen_target = "5.1",
|
||||||
|
}
|
51
lua/types/colors.d.tl
Normal file
51
lua/types/colors.d.tl
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
global record colors
|
||||||
|
white: number
|
||||||
|
orange: number
|
||||||
|
magenta: number
|
||||||
|
lightBlue: number
|
||||||
|
yellow: number
|
||||||
|
lime: number
|
||||||
|
pink: number
|
||||||
|
gray: number
|
||||||
|
lightGray: number
|
||||||
|
cyan: number
|
||||||
|
purple: number
|
||||||
|
blue: number
|
||||||
|
brown: number
|
||||||
|
green: number
|
||||||
|
red: number
|
||||||
|
black: number
|
||||||
|
|
||||||
|
combine: function(...: number): number
|
||||||
|
subtract: function(colors: number, ...: number): number
|
||||||
|
test: function(colors: number, color: number): boolean
|
||||||
|
packRGB: function(r: number, g: number, b: number): number
|
||||||
|
unpackRGB: function(rgb: number): number, number, number
|
||||||
|
toBlit: function(color: number): string
|
||||||
|
end
|
||||||
|
|
||||||
|
global record colours
|
||||||
|
white: number
|
||||||
|
orange: number
|
||||||
|
magenta: number
|
||||||
|
lightBlue: number
|
||||||
|
yellow: number
|
||||||
|
lime: number
|
||||||
|
pink: number
|
||||||
|
grey: number
|
||||||
|
lightGrey: number
|
||||||
|
cyan: number
|
||||||
|
purple: number
|
||||||
|
blue: number
|
||||||
|
brown: number
|
||||||
|
green: number
|
||||||
|
red: number
|
||||||
|
black: number
|
||||||
|
|
||||||
|
combine: function(...: number): number
|
||||||
|
subtract: function(colors: number, ...: number): number
|
||||||
|
test: function(colors: number, color: number): boolean
|
||||||
|
packRGB: function(r: number, g: number, b: number): number
|
||||||
|
unpackRGB: function(rgb: number): number, number, number
|
||||||
|
toBlit: function(color: number): string
|
||||||
|
end
|
70
lua/types/http.d.tl
Normal file
70
lua/types/http.d.tl
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
global record http
|
||||||
|
record Response
|
||||||
|
getResponseCode: function(): integer, string
|
||||||
|
getResponseHeaders: function(): {string: string}
|
||||||
|
read: function(count: integer | nil): integer, string | nil
|
||||||
|
readAll: function(): string | nil
|
||||||
|
readLine: function(withTrailing: boolean | nil): string | nil
|
||||||
|
seek: function(
|
||||||
|
whence: string | nil,
|
||||||
|
offset: integer | nil
|
||||||
|
): integer | nil, nil | string
|
||||||
|
close: function()
|
||||||
|
end
|
||||||
|
|
||||||
|
record _RequestParams
|
||||||
|
url: string
|
||||||
|
body: string | nil
|
||||||
|
headers: {string: string} | nil
|
||||||
|
binary: boolean | nil
|
||||||
|
method: string | nil
|
||||||
|
redirect: boolean | nil
|
||||||
|
end
|
||||||
|
|
||||||
|
request: function(
|
||||||
|
url_or_params: string | _RequestParams,
|
||||||
|
body: string | nil,
|
||||||
|
headers: {string: string} | nil,
|
||||||
|
binary: boolean | nil
|
||||||
|
)
|
||||||
|
|
||||||
|
record _GetParams
|
||||||
|
url: string
|
||||||
|
headers: {string: string} | nil
|
||||||
|
binary: boolean | nil
|
||||||
|
method: string | nil
|
||||||
|
redirect: boolean | nil
|
||||||
|
end
|
||||||
|
|
||||||
|
get: function(
|
||||||
|
url_or_params: string | _GetParams,
|
||||||
|
headers: {string: string} | nil,
|
||||||
|
binary: boolean | nil
|
||||||
|
): Response | nil, nil | string, nil | Response
|
||||||
|
|
||||||
|
post: function(
|
||||||
|
url_or_params: string | _RequestParams,
|
||||||
|
body: string | nil,
|
||||||
|
headers: {string: string} | nil,
|
||||||
|
binary: boolean | nil
|
||||||
|
): Response | nil, nil | string, nil | Response
|
||||||
|
|
||||||
|
checkURLAsync: function(url: string): boolean, string | nil
|
||||||
|
checkURL: function(url: string): boolean, string | nil
|
||||||
|
|
||||||
|
record Websocket
|
||||||
|
receive: function(timeout: number | nil): string | nil, nil | boolean
|
||||||
|
send: function(message: any, binary: boolean | nil)
|
||||||
|
close: function()
|
||||||
|
end
|
||||||
|
|
||||||
|
websocket: function(
|
||||||
|
url: string,
|
||||||
|
headers: {string: string} | nil
|
||||||
|
): Websocket | boolean, nil | string
|
||||||
|
|
||||||
|
websocketAsync: function(
|
||||||
|
url: string,
|
||||||
|
headers: {string: string} | nil
|
||||||
|
)
|
||||||
|
end
|
27
lua/types/os.d.tl
Normal file
27
lua/types/os.d.tl
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
global record os
|
||||||
|
pullEvent: function(filter: string | nil): string, any...
|
||||||
|
pullEventRaw: function(filter: string | nil): string, any...
|
||||||
|
sleep: function(time: number)
|
||||||
|
version: function(): string
|
||||||
|
run: function(env: table, path: string, ...: any): boolean
|
||||||
|
queueEvent: function(name: string, ...: any)
|
||||||
|
startTimer: function(time: number): integer
|
||||||
|
cancelTimer: function(token: integer)
|
||||||
|
setAlarm: function(time: number): integer
|
||||||
|
cancelAlarm: function(token: integer)
|
||||||
|
shutdown: function()
|
||||||
|
reboot: function()
|
||||||
|
getComputerID: function(): integer
|
||||||
|
computerID: function(): integer
|
||||||
|
getComputerLabel: function(): string
|
||||||
|
computerLabel: function(): string
|
||||||
|
setComputerLabel: function(label: string | nil)
|
||||||
|
clock: function(): number
|
||||||
|
time: function(locale: string | nil): number
|
||||||
|
time: function(locale: table): integer
|
||||||
|
day: function(args: string | nil): integer
|
||||||
|
epoch: function(args: string | nil): integer
|
||||||
|
date: function(): string
|
||||||
|
date: function(format: string): string | table
|
||||||
|
date: function(format: string, time: number): string | table
|
||||||
|
end
|
4
lua/types/parallel.d.tl
Normal file
4
lua/types/parallel.d.tl
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
global record parallel
|
||||||
|
waitForAny: function(...: function)
|
||||||
|
waitForAll: function(...: function)
|
||||||
|
end
|
33
lua/types/shell.d.tl
Normal file
33
lua/types/shell.d.tl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
global record shell
|
||||||
|
execute: function(command: string, ...: string): boolean
|
||||||
|
run: function(...: string): boolean
|
||||||
|
exit: function()
|
||||||
|
dir: function(): string
|
||||||
|
setDir: function(dir: string)
|
||||||
|
path: function(): string
|
||||||
|
setPath: function(path: string)
|
||||||
|
resolve: function(path: string): string
|
||||||
|
resolveProgram: function(command: string): string | nil
|
||||||
|
programs: function(include_hidden: boolean | nil): {string}
|
||||||
|
complete: function(sLine: string): {string} | nil
|
||||||
|
completeProgram: function(program: string): {string}
|
||||||
|
|
||||||
|
type _CompletionFunction = function(
|
||||||
|
shell: table,
|
||||||
|
index: integer,
|
||||||
|
argument: string,
|
||||||
|
previous: {string}
|
||||||
|
): {string} | nil
|
||||||
|
setCompletionFunction: function(program: string, complete: _CompletionFunction)
|
||||||
|
|
||||||
|
record _CompletionInfo
|
||||||
|
fnComplete: function
|
||||||
|
end
|
||||||
|
getCompletionInfo: function(): {string: _CompletionInfo}
|
||||||
|
getRunningProgram: function(): string
|
||||||
|
setAlias: function(command: string, program: string)
|
||||||
|
clearAlias: function(command: string)
|
||||||
|
aliases: function(): {string:string}
|
||||||
|
openTab: function(...: string): integer
|
||||||
|
switchTab: function(id: integer)
|
||||||
|
end
|
65
lua/types/term.d.tl
Normal file
65
lua/types/term.d.tl
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
global record term
|
||||||
|
|
||||||
|
record Redirect
|
||||||
|
write: function(text: string)
|
||||||
|
blit: function(text: string, textColor: string, backgroundColor: string)
|
||||||
|
scroll: function(y: integer)
|
||||||
|
clear: function()
|
||||||
|
clearLine: function()
|
||||||
|
|
||||||
|
getCursorPos: function(): integer, integer
|
||||||
|
setCursorPos: function(x: integer, y: integer)
|
||||||
|
getCursorBlink: function(): boolean
|
||||||
|
setCursorBlink: function(blink: boolean)
|
||||||
|
getSize: function(): integer, integer
|
||||||
|
|
||||||
|
isColor: function(): boolean
|
||||||
|
isColour: function(): boolean
|
||||||
|
getTextColor: function(): number
|
||||||
|
getTextColour: function(): number
|
||||||
|
setTextColor: function(color: number)
|
||||||
|
setTextColour: function(colour: number)
|
||||||
|
getBackgroundColor: function(): number
|
||||||
|
getBackgroundColour: function(): number
|
||||||
|
setBackgroundColor: function(color: number)
|
||||||
|
setBackgroundColour: function(colour: number)
|
||||||
|
getPaletteColor: function(color: number): number, number, number
|
||||||
|
getPaletteColour: function(colour: number): number, number, number
|
||||||
|
setPaletteColour: function(color: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
setPaletteColor: function(colour: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
write: function(text: string)
|
||||||
|
blit: function(text: string, textColor: string, backgroundColor: string)
|
||||||
|
scroll: function(y: integer)
|
||||||
|
clear: function()
|
||||||
|
clearLine: function()
|
||||||
|
|
||||||
|
getCursorPos: function(): integer, integer
|
||||||
|
setCursorPos: function(x: integer, y: integer)
|
||||||
|
getCursorBlink: function(): boolean
|
||||||
|
setCursorBlink: function(blink: boolean)
|
||||||
|
getSize: function(): integer, integer
|
||||||
|
|
||||||
|
isColor: function(): boolean
|
||||||
|
isColour: function(): boolean
|
||||||
|
getTextColor: function(): number
|
||||||
|
getTextColour: function(): number
|
||||||
|
setTextColor: function(color: number)
|
||||||
|
setTextColour: function(colour: number)
|
||||||
|
getBackgroundColor: function(): number
|
||||||
|
getBackgroundColour: function(): number
|
||||||
|
setBackgroundColor: function(color: number)
|
||||||
|
setBackgroundColour: function(colour: number)
|
||||||
|
getPaletteColor: function(color: number): number, number, number
|
||||||
|
getPaletteColour: function(colour: number): number, number, number
|
||||||
|
setPaletteColour: function(color: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
setPaletteColor: function(colour: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
|
||||||
|
nativePaletteColor: function(color: number): number, number, number
|
||||||
|
nativePaletteColour: function(colour: number): number, number, number
|
||||||
|
|
||||||
|
redirect: function(target: Redirect): Redirect
|
||||||
|
current: function(): Redirect
|
||||||
|
native: function(): Redirect
|
||||||
|
end
|
40
lua/types/window.d.tl
Normal file
40
lua/types/window.d.tl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
global record window
|
||||||
|
record Window
|
||||||
|
write: function(text: string)
|
||||||
|
blit: function(text: string, textColor: string, backgroundColor: string)
|
||||||
|
scroll: function(y: integer)
|
||||||
|
clear: function()
|
||||||
|
clearLine: function()
|
||||||
|
|
||||||
|
getCursorPos: function(): integer, integer
|
||||||
|
setCursorPos: function(x: integer, y: integer)
|
||||||
|
getCursorBlink: function(): boolean
|
||||||
|
setCursorBlink: function(blink: boolean)
|
||||||
|
getSize: function(): integer, integer
|
||||||
|
|
||||||
|
isColor: function(): boolean
|
||||||
|
isColour: function(): boolean
|
||||||
|
getTextColor: function(): number
|
||||||
|
getTextColour: function(): number
|
||||||
|
setTextColor: function(color: number)
|
||||||
|
setTextColour: function(colour: number)
|
||||||
|
getBackgroundColor: function(): number
|
||||||
|
getBackgroundColour: function(): number
|
||||||
|
setBackgroundColor: function(color: number)
|
||||||
|
setBackgroundColour: function(colour: number)
|
||||||
|
getPaletteColor: function(color: number): number, number, number
|
||||||
|
getPaletteColour: function(colour: number): number, number, number
|
||||||
|
setPaletteColour: function(color: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
setPaletteColor: function(colour: number, r_rgb: number, g: number | nil, b: number | nil)
|
||||||
|
|
||||||
|
getLine: function(y: integer): string, string, string
|
||||||
|
setVisible: function(visible: boolean)
|
||||||
|
isVisible: function(): boolean
|
||||||
|
redraw: function()
|
||||||
|
restoreCursor: function()
|
||||||
|
getPosition: function(): integer, integer
|
||||||
|
reposition: function(new_x: integer, new_y: integer, new_width: integer | nil, new_height: integer | nil, new_parent: term.Redirect | nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
create: function(parent: term.Redirect, x: integer, y: integer, width: integer, height: integer, startVisible: boolean | nil): Window
|
||||||
|
end
|
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
secret.env
|
secret.env
|
||||||
unmined-out
|
unmined-out
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
|
db.env
|
||||||
|
@ -13,7 +13,7 @@ Requires [poetry](https://archlinux.org/packages/community/any/python-poetry/)
|
|||||||
$ cd ../frontend
|
$ cd ../frontend
|
||||||
$ npm run dev
|
$ npm run dev
|
||||||
# in a different terminal
|
# in a different terminal
|
||||||
$ DEV_MODE=1 poetry run uvicorn server:app --reload
|
$ DEV_MODE=1 poetry run uvicorn server:app --reload --env-file db.env
|
||||||
```
|
```
|
||||||
|
|
||||||
### prod
|
### prod
|
||||||
@ -21,5 +21,5 @@ $ DEV_MODE=1 poetry run uvicorn server:app --reload
|
|||||||
$ pushd ../frontend
|
$ pushd ../frontend
|
||||||
$ npm run build
|
$ npm run build
|
||||||
$ popd
|
$ popd
|
||||||
$ poetry run uvicorn server:app
|
$ poetry run uvicorn server:app --env-file db.env
|
||||||
```
|
```
|
||||||
|
1
server/db.env.example
Normal file
1
server/db.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
MONGO_URI="mongodb://user:pass@localhost:27017/"
|
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
server/lua
Symbolic link
1
server/lua
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../lua/out
|
112
server/poetry.lock
generated
112
server/poetry.lock
generated
@ -196,6 +196,26 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "motor"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Non-blocking MongoDB driver for Tornado or asyncio"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pymongo = ">=4.1,<5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
aws = ["pymongo[aws] (>=4.1,<5)"]
|
||||||
|
encryption = ["pymongo[encryption] (>=4.1,<5)"]
|
||||||
|
gssapi = ["pymongo[gssapi] (>=4.1,<5)"]
|
||||||
|
ocsp = ["pymongo[ocsp] (>=4.1,<5)"]
|
||||||
|
snappy = ["pymongo[snappy] (>=4.1,<5)"]
|
||||||
|
srv = ["pymongo[srv] (>=4.1,<5)"]
|
||||||
|
zstd = ["pymongo[zstd] (>=4.1,<5)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.21"
|
version = "2.21"
|
||||||
@ -219,6 +239,23 @@ typing-extensions = ">=4.1.0"
|
|||||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||||
email = ["email-validator (>=1.0.3)"]
|
email = ["email-validator (>=1.0.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymongo"
|
||||||
|
version = "4.2.0"
|
||||||
|
description = "Python driver for MongoDB <http://www.mongodb.org>"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
aws = ["pymongo-auth-aws (<2.0.0)"]
|
||||||
|
encryption = ["pymongocrypt (>=1.3.0,<2.0.0)"]
|
||||||
|
gssapi = ["pykerberos"]
|
||||||
|
ocsp = ["certifi", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
|
||||||
|
snappy = ["python-snappy"]
|
||||||
|
srv = ["dnspython (>=1.16.0,<3.0.0)"]
|
||||||
|
zstd = ["zstandard"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@ -339,7 +376,7 @@ python-versions = ">=3.7"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "302816b99259a9b9b37589635b575875a945d1d5ebead0f9ea7795837cd62e71"
|
content-hash = "d47fdb1bb98fcd0b8e33a25f1e2e78d2ebfc2a568ad7b8b08a553987e46a2fd5"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
anyio = [
|
anyio = [
|
||||||
@ -558,6 +595,10 @@ MarkupSafe = [
|
|||||||
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
|
||||||
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
|
||||||
]
|
]
|
||||||
|
motor = [
|
||||||
|
{file = "motor-3.0.0-py3-none-any.whl", hash = "sha256:b076de44970f518177f0eeeda8b183f52eafa557775bfe3294e93bda18867a71"},
|
||||||
|
{file = "motor-3.0.0.tar.gz", hash = "sha256:3e36d29406c151b61342e6a8fa5e90c00c4723b76e30f11276a4373ea2064b7d"},
|
||||||
|
]
|
||||||
pycparser = [
|
pycparser = [
|
||||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||||
@ -600,6 +641,75 @@ pydantic = [
|
|||||||
{file = "pydantic-1.10.1-py3-none-any.whl", hash = "sha256:f8b10e59c035ff3dcc9791619d6e6c5141e0fa5cbe264e19e267b8d523b210bf"},
|
{file = "pydantic-1.10.1-py3-none-any.whl", hash = "sha256:f8b10e59c035ff3dcc9791619d6e6c5141e0fa5cbe264e19e267b8d523b210bf"},
|
||||||
{file = "pydantic-1.10.1.tar.gz", hash = "sha256:d41bb80347a8a2d51fbd6f1748b42aca14541315878447ba159617544712f770"},
|
{file = "pydantic-1.10.1.tar.gz", hash = "sha256:d41bb80347a8a2d51fbd6f1748b42aca14541315878447ba159617544712f770"},
|
||||||
]
|
]
|
||||||
|
pymongo = [
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b9e4981a65f8500a3a46bb3a1e81b9feb45cf0b2115ad9c4f8d517326d026940"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1c81414b706627f15e921e29ae2403aab52e33e36ed92ed989c602888d7c3b90"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:c549bb519456ee230e92f415c5b4d962094caac0fdbcc4ed22b576f66169764e"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:70216ec4c248213ae95ea499b6314c385ce01a5946c448fb22f6c8395806e740"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:8a86e8c2ac2ec87141e1c6cb00bdb18a4560f06e5f96769abcd1dda24dc0e764"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:314b556afd72eb21a6a10bd1f45ef252509f014f80207db59c97372103c88237"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:902e2c9030cb042c49750bc70d72d830d42c64ea0df5ff8630c171e065c93dd7"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c69ef5906dcd6ec565d4d887ba97ceb2a84f3b614307ee3b4780cb1ea40b1867"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07564178ecc203a84f63e72972691af6c0c82d2dc0c9da66ba711695276089ba"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47d5f10922cf7f7dfcd1406bd0926cef6d866a75953c3745502dffd7ac197dd"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cadaaa5c19ad23fc84559e90284f2eb003c36958ebb2c06f286b678f441285f"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d94f535df9f539615bc3dbbef185ded3b609373bb44ca1afffcabac70202678a"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:147a23cd96feb67606ac957744d8d25b013426cdc3c7164a4f99bd8253f649e3"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-win32.whl", hash = "sha256:ecdcb0d4e9b08b739035f57a09330efc6f464bd7f942b63897395d996ca6ebd5"},
|
||||||
|
{file = "pymongo-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c223aea52c359cc8fdee5bd3475532590755c269ec4d4fe581acd47a44e9952"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:fe0820d169635e41c14a5d21514282e0b93347878666ec9d5d3bf0eed0649948"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e39cacee70a98758f9b2da53ee175378f07c60113b1fa4fae40cbaee5583181e"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:701d331060dae72bf3ebdb82924405d14136a69282ccb00c89fc69dee21340b4"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e08fe1731f5429435b8dea1db9663f9ed1812915ff803fc9991c7c4841ed62ad"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:60c470a58c5b62b1b12a5f5458f8e2f2f67b94e198d03dc5352f854d9230c394"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:b211e161b6cc2790e0d640ad38e0429d06c944e5da23410f4dc61809dba25095"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:ed90a9de4431cbfb2f3b2ef0c5fd356e61c85117b2be4db3eae28cb409f6e2d5"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:68e1e49a5675748233f7b05330f092582cd52f2850b4244939fd75ba640593ed"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:764fc15418d94bce5c2f8ebdbf66544f96f42efb1364b61e715e5b33281b388d"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e64442aba81ed4df1ca494b87bf818569a1280acaa73071c68014f7a884e83f1"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83168126ae2457d1a19b2af665cafa7ef78c2dcff192d7d7b5dad6b36c73ae24"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69d0180bca594e81cdb4a2af328bdb4046f59e10aaeef7619496fe64f2ec918c"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80cbf0b043061451660099fff9001a7faacb2c9c983842b4819526e2f944dc6c"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e1b8f5e2f9637492b0da4d51f78ecb17786e61d6c461ead8542c944750faf4f9"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1a957cdc2b26eeed4d8f1889a40c6023dd1bd94672dd0f5ce327314f2caaefd4"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-win32.whl", hash = "sha256:6bd5888997ea3eae9830c6cc7964b61dcfbc50eb3a5a6ce56ad5f86d5579b11c"},
|
||||||
|
{file = "pymongo-4.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dc24737d24ce0de762bee9c2a884639819485f679bbac8ab5be9c161ef6f9b2c"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193cc97d44b1e6d2253ea94e30c6f94f994efb7166e2452af4df55825266e88b"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e152c26ffc30331e9d57591fc4c05453c209aa20ba299d1deb7173f7d1958c22"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a9bc4dcfc2bda69ee88cdb7a89b03f2b8eca668519b704384a264dea2db4209"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8cbb868e88c4eee1c53364bb343d226a3c0e959e791e6828030cb78f46cfcbe3"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:2bfe6b59f431f40fa545547616f4acf0c0c4b64518b1f951083e3bad06eb368b"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:ff66014687598823b6b23751884b4aa67eb934445406d95894dfc60cb7bfcc18"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:31c50da4a080166bc29403aa91f4c76e0889b4f24928d1b60508a37c1bf87f9a"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ccfdc7722df445c49dc6b5d514c3544cad99b53189165f7546793933050ac7fb"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc7ebc37b03956a070260665079665eae69e5e96007694214f3a2107af96816a"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8b4a782aac43948308087b962c9ecb030ba98886ce6dee3ad7aafe8c5e1ce80"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1c23527f8e13f526fededbb96f2e7888f179fe27c51d41c2724f7059b75b2fa"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cc3c35aeeceb67143914db67f685206e1aa37ea837d872f4bc28d7f80917c9"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e09cdf5aad507c8faa30d97884cc42932ed3a9c2b7f22cc3ccc607bae03981b3"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0f53253f4777cbccc426e669a2af875f26c95bd090d88593287b9a0a8ac7fa25"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21238b19243a42f9a34a6d39e7580ceebc6da6d2f3cf729c1cff9023cb61a5f1"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-win32.whl", hash = "sha256:766acb5b1a19eae0f7467bcd3398748f110ea5309cdfc59faa5185dcc7fd4dca"},
|
||||||
|
{file = "pymongo-4.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:10f09c4f09757c2e2a707ad7304f5d69cb8fdf7cbfb644dbacfe5bbe8afe311b"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6bf01b9237f794fa3bdad5089474067d28be7e199b356a18d3f247a45775f26"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d8bb745321716e7a11220a67c88212ecedde4021e1de4802e563baef9df921d2"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3be53e9888e759c49ae35d747ff77a04ff82b894dd64601e0f3a5a159b406245"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a3efdf154844244e0dabe902cf1827fdced55fa5b144adec2a86e5ce50a99b97"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:a7eb5b06744b911b6668b427c8abc71b6d624e72d3dfffed00988fa1b4340f97"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:b0be613d926c5dbb0d3fc6b58e4f2be4979f80ae76fda6e47309f011b388fe0c"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:e7dcb73f683c155885a3488646fcead3a895765fed16e93c9b80000bc69e96cb"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b537dd282de1b53d9ae7cf9f3df36420c8618390f2da92100391f3ba8f3c141a"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d98d2a8283c9928a9e5adf2f3c0181e095579e9732e1613aaa55d386e2bcb6c5"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76892bbce743eb9f90360b3626ea92f13d338010a1004b4488e79e555b339921"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:124d0e880b66f9b0778613198e89984984fdd37a3030a9007e5f459a42dfa2d3"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:773467d25c293f8e981b092361dab5fd800e1ba318403b7959d35004c67faedc"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6673ab3fbf3135cc1a8c0f70d480db5b2378c3a70af8d602f73f76b8338bdf97"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:153b8f8705970756226dfeeb7bb9637e0ad54a4d79b480b4c8244e34e16e1662"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:01721da74558f2f64a9f162ee063df403ed656b7d84229268d8e4ae99cfba59c"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-win32.whl", hash = "sha256:a25c0eb2d610b20e276e684be61c337396813b636b69373c17314283cb1a3b14"},
|
||||||
|
{file = "pymongo-4.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:44b36ccb90aac5ea50be23c1a6e8f24fbfc78afabdef114af16c6e0a80981364"},
|
||||||
|
{file = "pymongo-4.2.0.tar.gz", hash = "sha256:72f338f6aabd37d343bd9d1fdd3de921104d395766bcc5cdc4039e4c2dd97766"},
|
||||||
|
]
|
||||||
python-dotenv = [
|
python-dotenv = [
|
||||||
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
|
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
|
||||||
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
|
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
|
||||||
|
@ -13,6 +13,7 @@ Authlib = "^1.0.1"
|
|||||||
httpx = "^0.23.0"
|
httpx = "^0.23.0"
|
||||||
itsdangerous = "^2.1.2"
|
itsdangerous = "^2.1.2"
|
||||||
Jinja2 = "^3.1.2"
|
Jinja2 = "^3.1.2"
|
||||||
|
motor = "^3.0.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -1,24 +1,42 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||||
|
|
||||||
from .settings import settings
|
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, 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("/ipmi/", monitoring)
|
||||||
|
|
||||||
|
installer = j2env.get_template("install.lua").render(deploy_path=settings.deploy_path)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/install")
|
||||||
|
async def get_installer():
|
||||||
|
return PlainTextResponse(installer)
|
||||||
|
|
||||||
|
|
||||||
|
app.mount("/lua/", StaticFiles(directory=settings.lua_out_path))
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def on_startup():
|
||||||
|
asyncio.get_running_loop().create_task(ws_manager.queue_task())
|
||||||
|
|
||||||
|
|
||||||
frontend = FastAPI()
|
frontend = FastAPI()
|
||||||
|
|
||||||
manifest = dict()
|
manifest = dict()
|
||||||
if not settings.dev_mode:
|
if not settings.dev_mode:
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
with open(f"{settings.frontend_path}/manifest.json", "r") as f:
|
with open(f"{settings.frontend_path}/manifest.json", "r") as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
|
|
||||||
|
24
server/server/db.py
Normal file
24
server/server/db.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from bson import ObjectId
|
||||||
|
import motor.motor_asyncio as motor
|
||||||
|
from .settings import settings
|
||||||
|
|
||||||
|
client: motor.AsyncIOMotorClient = motor.AsyncIOMotorClient(settings.mongo_uri)
|
||||||
|
|
||||||
|
db = client["controlpanel"]
|
||||||
|
events: motor.AsyncIOMotorCollection = db["events"]
|
||||||
|
|
||||||
|
|
||||||
|
class PyObjectId(ObjectId):
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
yield cls.validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, v):
|
||||||
|
if not ObjectId.is_valid(v):
|
||||||
|
raise ValueError("Invalid objectid")
|
||||||
|
return ObjectId(v)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __modify_schema__(cls, field_schema):
|
||||||
|
field_schema.update(type="string")
|
175
server/server/monitoring.py
Normal file
175
server/server/monitoring.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import datetime
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
Body,
|
||||||
|
FastAPI,
|
||||||
|
HTTPException,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketDisconnect,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
from .db import PyObjectId, events
|
||||||
|
|
||||||
|
monitoring = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenContent(BaseModel):
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
blink: bool
|
||||||
|
fg: int
|
||||||
|
text: list[str]
|
||||||
|
fg_color: list[str]
|
||||||
|
bg_color: list[str]
|
||||||
|
palette: list[int]
|
||||||
|
|
||||||
|
|
||||||
|
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 send_connect(self, uuid: UUID):
|
||||||
|
if uuid not in self.computers:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.computers[uuid].send_json({"type": "viewer_connect"})
|
||||||
|
|
||||||
|
async def send_disconnect(self, uuid: UUID):
|
||||||
|
if uuid not in self.computers:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.computers[uuid].send_json({"type": "viewer_disconnect"})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if len(self.viewers.get(uuid, [])) > 0:
|
||||||
|
await self.send_connect(uuid)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if len(self.viewers[uuid]) == 0:
|
||||||
|
await self.send_connect(uuid)
|
||||||
|
|
||||||
|
self.viewers[uuid].add(socket)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = await socket.receive_json()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.viewers[uuid].remove(socket)
|
||||||
|
if len(self.viewers[uuid]) == 0:
|
||||||
|
await self.send_disconnect(uuid)
|
||||||
|
|
||||||
|
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()
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
|
||||||
|
timestamp: datetime
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
json_encoders = {ObjectId: str}
|
||||||
|
|
||||||
|
|
||||||
|
@monitoring.get("/events", response_model=list[Event])
|
||||||
|
async def get_events():
|
||||||
|
print("get /events")
|
||||||
|
return await events.find().to_list(1000)
|
||||||
|
|
||||||
|
|
||||||
|
@monitoring.get("/events/{id}", response_model=Event)
|
||||||
|
async def get_single_event(id: PyObjectId):
|
||||||
|
if (event := await events.find_one({"_id": id})) is not None:
|
||||||
|
return event
|
||||||
|
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@monitoring.post(
|
||||||
|
"/push_event", response_model=Event, status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
async def push_event(value: Any = Body(...)):
|
||||||
|
event = {
|
||||||
|
"timestamp": datetime.now(),
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
new_event = await events.insert_one(event)
|
||||||
|
created_event = await events.find_one({"_id": new_event.inserted_id})
|
||||||
|
return created_event
|
@ -7,6 +7,11 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
frontend_path: str = "frontend"
|
frontend_path: str = "frontend"
|
||||||
unmined_out_path: str = "unmined-out"
|
unmined_out_path: str = "unmined-out"
|
||||||
|
lua_out_path: str = "lua"
|
||||||
|
|
||||||
|
mongo_uri: str
|
||||||
|
|
||||||
|
deploy_path: str = "http://localhost:8000"
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
6
server/templates/install.lua
Normal file
6
server/templates/install.lua
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
local path = "{{ deploy_path }}"
|
||||||
|
files = { "main.lua", "json.lua", "framebuffer.lua", "ringbuffer.lua", "socket.lua" }
|
||||||
|
for _, file in ipairs(files) do
|
||||||
|
fs.delete(file)
|
||||||
|
shell.run(("wget %s/lua/%s"):format(path, file))
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user