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

View File

@ -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'
: ''}" : ''}"
> >

View File

@ -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;
} }

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 secret.env
unmined-out unmined-out
**/__pycache__ **/__pycache__
db.env

View File

@ -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
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 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"},

View File

@ -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]

View File

@ -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
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" 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()

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