Merge branch 'feature/monitoring'

This commit is contained in:
Kai Vogelgesang 2022-09-25 14:43:25 +02:00
commit 8886b23434
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
43 changed files with 1958 additions and 17 deletions

1
db/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
secret.env

19
db/docker-compose.yml Normal file
View 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
View 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/"

View File

@ -9,6 +9,8 @@
import Mining from "./pages/mining/Mining.svelte";
import Footer from "./Footer.svelte";
import BaseLayout from "./BaseLayout.svelte";
import Monitoring from "./pages/monitoring/Monitoring.svelte";
import Bar from "./pages/playground/Bar.svelte";
onMount(async () => {
const res = await fetch("/user/me");
@ -25,7 +27,7 @@
</BaseLayout>
</Route>
<Route path="/foo">
<Route path="foo">
<BaseLayout>
<section class="hero is-danger is-fullheight">
<div class="hero-body">
@ -38,19 +40,19 @@
</BaseLayout>
</Route>
<Route path="/bar">
<BaseLayout>bar</BaseLayout>
<Route path="bar">
<BaseLayout><Bar /></BaseLayout>
</Route>
<Route path="/monitoring">
<BaseLayout>monitoring</BaseLayout>
<Route path="monitoring/*">
<BaseLayout><Monitoring /></BaseLayout>
</Route>
<Route path="/mining">
<Route path="mining">
<Mining />
</Route>
<Route path="/stats">
<Route path="stats">
<BaseLayout>stats</BaseLayout>
</Route>

View File

@ -10,7 +10,7 @@
<Link
to={target}
class="navbar-item is-tab {$location.pathname === target
class="navbar-item is-tab {$location.pathname.startsWith(target)
? 'is-active'
: ''}"
>

View File

@ -18,6 +18,11 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/v4-shims.scss";
// https://github.com/mefechoel/svelte-navigator#what-are-the-weird-rectangles-around-the-headings-in-my-app
h1:focus {
h1:focus,
h2:focus,
h3:focus,
h4:focus,
h5:focus,
h6:focus {
outline: none;
}
}

View 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>

View 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} />

View 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>

View 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);
}

View 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[]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>

View 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
View File

@ -0,0 +1 @@
out

20
lua/cc.d.tl Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
return {
global_env_def = "cc",
gen_target = "5.1",
}

51
lua/types/colors.d.tl Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
global record parallel
waitForAny: function(...: function)
waitForAll: function(...: function)
end

33
lua/types/shell.d.tl Normal file
View 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
View 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
View 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
View File

@ -2,3 +2,4 @@
secret.env
unmined-out
**/__pycache__
db.env

View File

@ -13,7 +13,7 @@ Requires [poetry](https://archlinux.org/packages/community/any/python-poetry/)
$ cd ../frontend
$ npm run dev
# 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
@ -21,5 +21,5 @@ $ DEV_MODE=1 poetry run uvicorn server:app --reload
$ pushd ../frontend
$ npm run build
$ popd
$ poetry run uvicorn server:app
```
$ poetry run uvicorn server:app --env-file db.env
```

1
server/db.env.example Normal file
View File

@ -0,0 +1 @@
MONGO_URI="mongodb://user:pass@localhost:27017/"

96
server/dummy-client.py Normal file
View 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
View File

@ -0,0 +1 @@
../lua/out

112
server/poetry.lock generated
View File

@ -196,6 +196,26 @@ category = "main"
optional = false
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]]
name = "pycparser"
version = "2.21"
@ -219,6 +239,23 @@ typing-extensions = ">=4.1.0"
dotenv = ["python-dotenv (>=0.10.4)"]
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]]
name = "python-dotenv"
version = "0.21.0"
@ -339,7 +376,7 @@ python-versions = ">=3.7"
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "302816b99259a9b9b37589635b575875a945d1d5ebead0f9ea7795837cd62e71"
content-hash = "d47fdb1bb98fcd0b8e33a25f1e2e78d2ebfc2a568ad7b8b08a553987e46a2fd5"
[metadata.files]
anyio = [
@ -558,6 +595,10 @@ MarkupSafe = [
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{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 = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{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.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 = [
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},

View File

@ -13,6 +13,7 @@ Authlib = "^1.0.1"
httpx = "^0.23.0"
itsdangerous = "^2.1.2"
Jinja2 = "^3.1.2"
motor = "^3.0.0"
[build-system]

View File

@ -1,24 +1,42 @@
import asyncio
import json
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 .user import user_auth
from .map_tiles import map_tiles, map_meta
from .templates import j2env
from .monitoring import monitoring, ws_manager
app = FastAPI()
app.mount("/user/", user_auth)
app.mount("/map/", map_meta)
app.mount("/tiles/", map_tiles)
app.mount("/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()
manifest = dict()
if not settings.dev_mode:
from fastapi.staticfiles import StaticFiles
with open(f"{settings.frontend_path}/manifest.json", "r") as f:
manifest = json.load(f)

24
server/server/db.py Normal file
View 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
View 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

View File

@ -7,6 +7,11 @@ class Settings(BaseSettings):
frontend_path: str = "frontend"
unmined_out_path: str = "unmined-out"
lua_out_path: str = "lua"
mongo_uri: str
deploy_path: str = "http://localhost:8000"
settings = Settings()

View 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