From 1fde73778bbd484ac261e79d547cd7e50fa17b5d Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Tue, 20 Sep 2022 12:16:46 +0200 Subject: [PATCH 1/9] Implement framebuffer and mock endpoint --- lua/cc.d.tl | 18 ++ lua/framebuffer.tl | 132 ++++++++++++ lua/json.d.tl | 6 + lua/json.lua | 388 ++++++++++++++++++++++++++++++++++++ lua/main.tl | 35 ++++ lua/tlconfig.lua | 4 + lua/types/colors.d.tl | 51 +++++ lua/types/http.d.tl | 70 +++++++ lua/types/parallel.d.tl | 4 + lua/types/shell.d.tl | 33 +++ lua/types/term.d.tl | 65 ++++++ lua/types/window.d.tl | 39 ++++ server/server/__init__.py | 2 + server/server/monitoring.py | 25 +++ 14 files changed, 872 insertions(+) create mode 100644 lua/cc.d.tl create mode 100644 lua/framebuffer.tl create mode 100644 lua/json.d.tl create mode 100644 lua/json.lua create mode 100644 lua/main.tl create mode 100644 lua/tlconfig.lua create mode 100644 lua/types/colors.d.tl create mode 100644 lua/types/http.d.tl create mode 100644 lua/types/parallel.d.tl create mode 100644 lua/types/shell.d.tl create mode 100644 lua/types/term.d.tl create mode 100644 lua/types/window.d.tl create mode 100644 server/server/monitoring.py diff --git a/lua/cc.d.tl b/lua/cc.d.tl new file mode 100644 index 0000000..85046f7 --- /dev/null +++ b/lua/cc.d.tl @@ -0,0 +1,18 @@ +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") \ No newline at end of file diff --git a/lua/framebuffer.tl b/lua/framebuffer.tl new file mode 100644 index 0000000..c342341 --- /dev/null +++ b/lua/framebuffer.tl @@ -0,0 +1,132 @@ +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 : {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 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 = win }) 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 true + end + + buffer.clear_dirty = function() end + + return buffer + +end + +return { wrap = wrap } \ No newline at end of file diff --git a/lua/json.d.tl b/lua/json.d.tl new file mode 100644 index 0000000..4449415 --- /dev/null +++ b/lua/json.d.tl @@ -0,0 +1,6 @@ +local record Json + encode: function(value: any): string + decode: function(str: string): any +end + +return Json \ No newline at end of file diff --git a/lua/json.lua b/lua/json.lua new file mode 100644 index 0000000..711ef78 --- /dev/null +++ b/lua/json.lua @@ -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 diff --git a/lua/main.tl b/lua/main.tl new file mode 100644 index 0000000..a64d32b --- /dev/null +++ b/lua/main.tl @@ -0,0 +1,35 @@ +local json = require("json") +local fb = require("framebuffer") +local ENDPOINT = "http://localhost:8000/monitoring" + +local orig_native = term.native + +local buffer = fb.wrap(orig_native()) + +term.native = function(): term.Redirect + return buffer.target +end + +term.redirect(buffer.target as term.Redirect) + +local function report() + while true do + local body = json.encode({ + screen = buffer.serialize() + }) + local headers = { + ["Content-Type"] = "application/json" + } + local r = { pcall(http.post, ENDPOINT .. "/ping", body, headers) } + sleep(1) + end +end + +local function run_shell() + shell.run("shell") +end + +parallel.waitForAny(report, run_shell) + +term.native = orig_native +term.redirect(term.native()) diff --git a/lua/tlconfig.lua b/lua/tlconfig.lua new file mode 100644 index 0000000..52911f6 --- /dev/null +++ b/lua/tlconfig.lua @@ -0,0 +1,4 @@ +return { + global_env_def = "cc", + gen_target = "5.1", +} \ No newline at end of file diff --git a/lua/types/colors.d.tl b/lua/types/colors.d.tl new file mode 100644 index 0000000..a6e122d --- /dev/null +++ b/lua/types/colors.d.tl @@ -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 \ No newline at end of file diff --git a/lua/types/http.d.tl b/lua/types/http.d.tl new file mode 100644 index 0000000..26677bb --- /dev/null +++ b/lua/types/http.d.tl @@ -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 \ No newline at end of file diff --git a/lua/types/parallel.d.tl b/lua/types/parallel.d.tl new file mode 100644 index 0000000..fbb3a80 --- /dev/null +++ b/lua/types/parallel.d.tl @@ -0,0 +1,4 @@ +global record parallel + waitForAny: function(...: function) + waitForAll: function(...: function) +end \ No newline at end of file diff --git a/lua/types/shell.d.tl b/lua/types/shell.d.tl new file mode 100644 index 0000000..51375ab --- /dev/null +++ b/lua/types/shell.d.tl @@ -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 \ No newline at end of file diff --git a/lua/types/term.d.tl b/lua/types/term.d.tl new file mode 100644 index 0000000..0f790a0 --- /dev/null +++ b/lua/types/term.d.tl @@ -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 \ No newline at end of file diff --git a/lua/types/window.d.tl b/lua/types/window.d.tl new file mode 100644 index 0000000..afd6d68 --- /dev/null +++ b/lua/types/window.d.tl @@ -0,0 +1,39 @@ +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() + 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 \ No newline at end of file diff --git a/server/server/__init__.py b/server/server/__init__.py index b485732..2b52354 100644 --- a/server/server/__init__.py +++ b/server/server/__init__.py @@ -6,12 +6,14 @@ 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 app = FastAPI() app.mount("/user/", user_auth) app.mount("/map/", map_meta) app.mount("/tiles/", map_tiles) +app.mount("/monitoring/", monitoring) frontend = FastAPI() diff --git a/server/server/monitoring.py b/server/server/monitoring.py new file mode 100644 index 0000000..933d645 --- /dev/null +++ b/server/server/monitoring.py @@ -0,0 +1,25 @@ +import binascii + +from fastapi import FastAPI, Request +from pydantic import BaseModel +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] + +class Ping(BaseModel): + screen: ScreenContent + +@monitoring.post("/ping") +async def ping(request: Request, data: Ping): + print("[PING]") + for line in data.screen.text: + print(line) \ No newline at end of file From 2af01367037a7f63f74103a85778f3f9d5ced72d Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Wed, 21 Sep 2022 10:33:16 +0200 Subject: [PATCH 2/9] Implement monitoring lua client --- lua/cc.d.tl | 4 +- lua/framebuffer.tl | 13 ++- lua/main.tl | 159 ++++++++++++++++++++++++++++++++---- lua/ringbuffer.tl | 41 ++++++++++ lua/types/os.d.tl | 27 ++++++ lua/types/window.d.tl | 1 + server/server/monitoring.py | 19 ++++- 7 files changed, 239 insertions(+), 25 deletions(-) create mode 100644 lua/ringbuffer.tl create mode 100644 lua/types/os.d.tl diff --git a/lua/cc.d.tl b/lua/cc.d.tl index 85046f7..9072ae2 100644 --- a/lua/cc.d.tl +++ b/lua/cc.d.tl @@ -15,4 +15,6 @@ require("types/term") require("types/parallel") require("types/http") require("types/shell") -require("types/window") \ No newline at end of file +require("types/window") +require("types/os") + diff --git a/lua/framebuffer.tl b/lua/framebuffer.tl index c342341..18edb00 100644 --- a/lua/framebuffer.tl +++ b/lua/framebuffer.tl @@ -46,10 +46,12 @@ local function wrap(parent: term.Redirect): Buffer 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 @@ -84,7 +86,12 @@ local function wrap(parent: term.Redirect): Buffer end overrides.setPaletteColour = overrides.setPaletteColor - local target = setmetatable(overrides, { __index = win }) as term.Redirect + 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) @@ -120,10 +127,10 @@ local function wrap(parent: term.Redirect): Buffer end buffer.is_dirty = function(): boolean - return true + return dirty end - buffer.clear_dirty = function() end + buffer.clear_dirty = function() dirty = false end return buffer diff --git a/lua/main.tl b/lua/main.tl index a64d32b..ec6ca82 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -1,35 +1,158 @@ local json = require("json") local fb = require("framebuffer") -local ENDPOINT = "http://localhost:8000/monitoring" +local ringbuffer = require("ringbuffer") +local UUID = "8b9faf9f-9470-4a50-b405-0af5f0152550" +local ENDPOINT = "ws://localhost:8000/monitoring/computer/" .. UUID .. "/ws" + +print("[MAIN] Init") + +local enum SocketState + "reset" + "connecting" -- currently unused + "ok" +end + +local record Socket + state: SocketState + ws: http.Websocket +end + +local socket: Socket = { + state = "reset", + ws = nil, +} + +local function send(message: string) + -- "message" needs to be valid JSON + -- otherwise the server will not accept it + + if socket.state ~= "ok" then return end + + local r = { pcall(socket.ws.send, message) } + + if r[1] == false then + if (r[2] as string):sub(-11) == "closed file" then + socket.state = "reset" + elseif (r[2] as string):sub(-9) == "too large" then + -- TODO handle + -- the connection stays open though + end + end +end + +-- Set up framebuffer capture and statusline + +print("[MAIN] Setup framebuffer") local orig_native = term.native - local buffer = fb.wrap(orig_native()) - term.native = function(): term.Redirect return buffer.target end -term.redirect(buffer.target as term.Redirect) +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 report() +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 ws_task = coroutine.create(function() while true do - local body = json.encode({ - screen = buffer.serialize() - }) - local headers = { - ["Content-Type"] = "application/json" - } - local r = { pcall(http.post, ENDPOINT .. "/ping", body, headers) } - sleep(1) + if socket.state == "reset" then + set_bar("[WS] RST", "78870111") + local r = http.websocket(ENDPOINT) + if r ~= false then + socket.ws = r as http.Websocket + set_bar("[WS] OK\x03", "78870DD5") + socket.state = "ok" + else + set_bar("[WS] ERR", "78870EEE") + end + end + repeat + sleep(1) + until socket.state ~= "ok" + end +end) + +local report_task = coroutine.create(function() + local last_report = -1.0 + while true do + local now = os.clock() + if now - last_report >= 0.05 then + local message = json.encode({ + screen = buffer.serialize() + }) + send(message) + last_report = now + end + sleep(0) -- until next gametick + end +end) + +local shell_task = coroutine.create(function() + shell.run("shell") +end) + +-- basically parallel.waitForAny + +local record Task + coro: thread + filter: string | nil +end + +local tasks: {Task} = { + {coro = shell_task}, -- pid 1 + {coro = ws_task}, + {coro = report_task}, +} + +local event_queue = ringbuffer.new(64) +event_queue:push({n = 0}) + +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 + + for pid = 1, #tasks do + local task = tasks[pid] + if task.filter == nil or task.filter == e[1] or e[1] == "terminate" then + local ok, param = coroutine.resume(task.coro, table.unpack(e as {any})) + if not ok then + term.redirect(orig_native()) + term.clear() + term.setCursorPos(1,1) + print("OMEGABIG OOF") + print(("pid %d"):format(pid)) + error(param, 0) + else + task.filter = param as string + end + if pid == 1 and coroutine.status(task.coro) == "dead" then + shell_running = false + end + end end end -local function run_shell() - shell.run("shell") -end - -parallel.waitForAny(report, run_shell) term.native = orig_native term.redirect(term.native()) +term.clear() +term.setCursorPos(1,1) \ No newline at end of file diff --git a/lua/ringbuffer.tl b/lua/ringbuffer.tl new file mode 100644 index 0000000..ca7afe0 --- /dev/null +++ b/lua/ringbuffer.tl @@ -0,0 +1,41 @@ +local record Ringbuffer + {T} + push: function(self: Ringbuffer, el: T): boolean + pop: function(self: Ringbuffer): T | nil + is_empty: function(self: Ringbuffer): boolean + + head: integer + n: integer + size: integer +end + +local impl: table = {} + +impl.push = function(self: Ringbuffer, 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(self: Ringbuffer): 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(self: Ringbuffer): boolean + return self.n == 0 +end + +local function new(size: integer): Ringbuffer + return setmetatable({ head = 0, n = 0, size = size }, { __index = impl }) +end + +return { + new = new +} \ No newline at end of file diff --git a/lua/types/os.d.tl b/lua/types/os.d.tl new file mode 100644 index 0000000..8d4e75f --- /dev/null +++ b/lua/types/os.d.tl @@ -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 \ No newline at end of file diff --git a/lua/types/window.d.tl b/lua/types/window.d.tl index afd6d68..91730d5 100644 --- a/lua/types/window.d.tl +++ b/lua/types/window.d.tl @@ -31,6 +31,7 @@ global record window 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 diff --git a/server/server/monitoring.py b/server/server/monitoring.py index 933d645..a55fd56 100644 --- a/server/server/monitoring.py +++ b/server/server/monitoring.py @@ -1,6 +1,7 @@ -import binascii +import json +from uuid import UUID -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect from pydantic import BaseModel monitoring = FastAPI() @@ -22,4 +23,16 @@ class Ping(BaseModel): async def ping(request: Request, data: Ping): print("[PING]") for line in data.screen.text: - print(line) \ No newline at end of file + print(line) + +@monitoring.websocket("/computer/{uuid}/ws") +async def computer_ws(socket: WebSocket, uuid: UUID): + await socket.accept() + print(f"[WS] Computer {uuid} connected") + while True: + try: + data = await socket.receive_json() + #print(f"[WS] rx {json.dumps(data)}") + except WebSocketDisconnect: + break + print(f"[WS] Computer {uuid} disconnected") \ No newline at end of file From 7fa635170a6acb395391ae3d4dec6fe673d52cda Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Thu, 22 Sep 2022 19:27:01 +0200 Subject: [PATCH 3/9] Implement monitoring infrastructure --- frontend/src/App.svelte | 16 ++- frontend/src/Navlink.svelte | 2 +- frontend/src/app.scss | 9 +- .../src/pages/monitoring/Monitoring.svelte | 27 ++++ frontend/src/pages/monitoring/Screen.svelte | 121 ++++++++++++++++++ frontend/src/pages/monitoring/Viewer.svelte | 65 ++++++++++ frontend/src/pages/monitoring/font.ts | 25 ++++ frontend/src/pages/monitoring/proto.ts | 12 ++ frontend/src/pages/monitoring/term_font.png | Bin 0 -> 3904 bytes frontend/src/pages/playground/Bar.svelte | 21 +++ frontend/src/pages/playground/Canvas.svelte | 24 ++++ lua/main.tl | 2 +- server/dummy-client.py | 96 ++++++++++++++ server/server/__init__.py | 11 +- server/server/monitoring.py | 97 +++++++++++--- 15 files changed, 499 insertions(+), 29 deletions(-) create mode 100644 frontend/src/pages/monitoring/Monitoring.svelte create mode 100644 frontend/src/pages/monitoring/Screen.svelte create mode 100644 frontend/src/pages/monitoring/Viewer.svelte create mode 100644 frontend/src/pages/monitoring/font.ts create mode 100644 frontend/src/pages/monitoring/proto.ts create mode 100644 frontend/src/pages/monitoring/term_font.png create mode 100644 frontend/src/pages/playground/Bar.svelte create mode 100644 frontend/src/pages/playground/Canvas.svelte create mode 100644 server/dummy-client.py diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index f6913ef..4ab0ec1 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -9,6 +9,8 @@ import Mining from "./pages/mining/Mining.svelte"; import Footer from "./Footer.svelte"; import BaseLayout from "./BaseLayout.svelte"; + import Monitoring from "./pages/monitoring/Monitoring.svelte"; + import Bar from "./pages/playground/Bar.svelte"; onMount(async () => { const res = await fetch("/user/me"); @@ -25,7 +27,7 @@ - +
@@ -38,19 +40,19 @@ - - bar + + - - monitoring + + - + - + stats diff --git a/frontend/src/Navlink.svelte b/frontend/src/Navlink.svelte index bfee97a..0c567ce 100644 --- a/frontend/src/Navlink.svelte +++ b/frontend/src/Navlink.svelte @@ -10,7 +10,7 @@ diff --git a/frontend/src/app.scss b/frontend/src/app.scss index 53bb026..b2eb317 100644 --- a/frontend/src/app.scss +++ b/frontend/src/app.scss @@ -18,6 +18,11 @@ $fa-font-path: "@fortawesome/fontawesome-free/webfonts"; @import "@fortawesome/fontawesome-free/scss/v4-shims.scss"; // https://github.com/mefechoel/svelte-navigator#what-are-the-weird-rectangles-around-the-headings-in-my-app -h1:focus { +h1:focus, +h2:focus, +h3:focus, +h4:focus, +h5:focus, +h6:focus { outline: none; -} +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/Monitoring.svelte b/frontend/src/pages/monitoring/Monitoring.svelte new file mode 100644 index 0000000..0701ece --- /dev/null +++ b/frontend/src/pages/monitoring/Monitoring.svelte @@ -0,0 +1,27 @@ + + +
+ +
+

Yo wassup

+
    + {#each uuids as id} +
  • {id}
  • + {/each} +
+
+
+ + +
+

ASSUMING DIRECT CONTROL

+ + fuck go back +
+
+
diff --git a/frontend/src/pages/monitoring/Screen.svelte b/frontend/src/pages/monitoring/Screen.svelte new file mode 100644 index 0000000..baa94ad --- /dev/null +++ b/frontend/src/pages/monitoring/Screen.svelte @@ -0,0 +1,121 @@ + + + diff --git a/frontend/src/pages/monitoring/Viewer.svelte b/frontend/src/pages/monitoring/Viewer.svelte new file mode 100644 index 0000000..3622cb0 --- /dev/null +++ b/frontend/src/pages/monitoring/Viewer.svelte @@ -0,0 +1,65 @@ + + + +
{JSON.stringify(data, null, 2)}
diff --git a/frontend/src/pages/monitoring/font.ts b/frontend/src/pages/monitoring/font.ts new file mode 100644 index 0000000..df4fad7 --- /dev/null +++ b/frontend/src/pages/monitoring/font.ts @@ -0,0 +1,25 @@ +export const charWidth = 6, charHeight = 9; + +export async function loadFont(src: RequestInfo | URL): Promise { + const fontImg = await fetch(src); + const fontBlob = await fontImg.blob(); + + async function getCharBitmap(x: number, y: number) { + const fontOffsetX = 1, fontOffsetY = 1, fontPaddingX = 2, fontPaddingY = 2; + + const offsetX = (charWidth + fontPaddingX) * x + fontOffsetX; + const offsetY = (charHeight + fontPaddingY) * y + fontOffsetY; + + return await createImageBitmap(fontBlob, offsetX, offsetY, charWidth, charHeight); + } + + const font = Array(256); + + for (let y = 0; y < 16; ++y) { + for (let x = 0; x < 16; ++x) { + const i = 16 * y + x; + font[i] = getCharBitmap(x, y); + } + } + return await Promise.all(font); +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/proto.ts b/frontend/src/pages/monitoring/proto.ts new file mode 100644 index 0000000..2eb5029 --- /dev/null +++ b/frontend/src/pages/monitoring/proto.ts @@ -0,0 +1,12 @@ +export type ScreenContent = { + x: number + y: number + width: number + height: number + blink: boolean + fg: number + text: string[] + fg_color: string[] + bg_color: string[] + palette: number[] +} \ No newline at end of file diff --git a/frontend/src/pages/monitoring/term_font.png b/frontend/src/pages/monitoring/term_font.png new file mode 100644 index 0000000000000000000000000000000000000000..7bf23be2e751a8ba4dc8f408e56f0522198e6d2d GIT binary patch literal 3904 zcmeHKc{tQ-8~)8O!cZYI5)ny~ZA6ZBEE!73a%>SzjiQolLup14BEunJ(pZXYQ<_oA zT9(N^)=3#A$~I#+w!}BS>-_!w|6S)i*Y&>d^E`jN%YEJV^IUJdy)8;qSXvkW08wiz z3kLuI@mvrf$j4j!OI@9Iq96y989?ronc@ZfzNTkQ0ic`!-SmL~03Tp)<8+Fb-`?Kl z&HviJ2>b&P@D1glcT^MXVC%>e!avyn$JJd*JPHWiv~mjqfZd;V21xSgX*>W3*H~Ma zI$^zLhzaRGi$yI8x3}{pjEWl@=%Kw~NgOQ$J|!WWWX}DX2qoOs>aAk%oybU?2ydD6 zsb1B@ITS?&Licn>8((P8_+%kpKmP6z%jYe=*EyCEnN|$^^*UfcoAA3ySLHi8V*muD zK=kFl?tZR`19$d`NKz`;$>)+zHgLK5Y~wSY_|S&(It7e7n-pbcmcdQFw%J_@TOE)+v^o!)Q)nUl3BRy*SH#mf@%>IpVIy9&WE}p zKq|;<4P{?4g2%t%Y}9P@2-B3a-*tPhS7y@MZk2-*CB(GOfed zrzs=*A|y8o5D&%7F(;Q)>m=C_t@d#$rs71BFVZ^NV)Wny)+V8QAht&_VjX)p=A%c> zS2@xCH*>~OULL_4*dMSvOVWnl$KyRO;{;7Fw5bD5DzTYvKj#Uxt^%SaP?&8r>B0gj zC>PMjKjYT%O|z0Wf;$HU0aD=9~WBdsI4UEGf)hw##`Wid2P9dyGUXCv;g8Y*r~{XY7=r#IG{4Le$*2^QsPAr z-1RUe7KWb-xuLvrwb(Q9N`fJF|E6D#&t8iwdcPYcFT}i09edl?S1pB*E1P8=`+6%* zLG#&fpJMHtX?DS~7U9WonXMV~XiVao?9l+wjpWwKo;9&Ti=o4#&UsDZpRVS)ZFbDz zJq}}=xAxwvP4@)Wi$?dUj+zX2x;Wo&1?hIWOzYi`b=5Hu!-@LQ9z`@ntBFwKlR%#DDZK8%(zRa3^B8W7+=CVC{+`U=6NLb!$BrV16I!#Yp z3#$1rqe<1^fChf{>dRQ3dc%f+ZHS<|_Eh&+iS8K)$|a%fcd1!5W<6 zVl7{u{d>-yLzDi4kmP|aaa3KK`oLyr0@LmL040ES8q_&)Kua?_a2H3JWzd+L$hf^W zy@8DHb$PVzoM+rAxba4WkNlqMi=VZ6ciBB_Car9?%-V$}NlCuBpNky%t4Mfn7?pW` zO=`lbvHOcE#wT=tO*qJs$R_fYsJb$VAy{y;TINRy)+Yqb zGq(jcqhF%BjKr&2BS2eXp`e^sKTm_waXmA+E%~O-XRQ!83Q(<1K54G68dJQ{GI=2y zo4=MTUV9*|rUVD zGf?z*iVB8isO4E&2#@=A3ZVz{e*D*}&TLBEK6!#inm10!jHv8-9sfDFI{xE%>8^`# zK>wS(!i3PWuIXg3VS*W&H6D3_NFqUZ_Llo#3B|(PLhraIb1g#Ck<0?s2z7JwNke zDp};p9*v_yTt&52tEVE*fLTIS^s8UwUCU^Ohl3O zyXQ?N$XBi#Dz;q5Z21CZ>8jSgX}WF36ONb8}&`8uIHQl%PoJZ zzU`mUXjZsSwwQ0N)<@WXc;6SBGZ%L?WNPpzCRkh6cs({;f!=)5fP@BN($3P#WF3!* z`B_d-l!AWiT&YVwlIop-YB}jn)Pp`gqk=!O#!MPVhkZt8${B*_xI{K&4K)avPi+ZI zMVW^95g&M)#jP~|h_KBO`xIr9WQU(^{f5Q8P#=!KEU*s7OQ;}+%KhEp^-k-S;Y_EX zh3&R>g0$`pCZqfhcv=WH`LY1ii)Sy*&}^a)N}@x$JoFNnBo zsbGQwUmDR&{^(QPdv&zWDJdX|J){sdvPcAPO7OICApr)|U6z^Cd!MG)KGe{N6{X7T zAVXqjYI{sHe34-N%Hp8%hc^}$Hodc0Eg_^+!gz*rxWA_p_(Ii(6m)eqsl7FJZ`P*%HdbO&3GQI6DDFF3BB1<@1w7ThQ(%l?{HyQVInf*N%Hm*F{wrjCrFtbv#| zhf9m^=kUe5E$5UM-h+qIb~HE$!SA%I|K*^&>q>t=@B~ViT$5>tD!_H3T%K?_6!f|i zZF;bc1NP3Q-7`!dgw*R3AmPU;P<+Ti=%OnI4zLYjKg!$Hfr0SjJd*w&Z_oyv=W=nQ XA!+xD{uor-`QTWevb7+adEfgF>y~f^ literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playground/Bar.svelte b/frontend/src/pages/playground/Bar.svelte new file mode 100644 index 0000000..3f756a7 --- /dev/null +++ b/frontend/src/pages/playground/Bar.svelte @@ -0,0 +1,21 @@ + + +
+ +
diff --git a/frontend/src/pages/playground/Canvas.svelte b/frontend/src/pages/playground/Canvas.svelte new file mode 100644 index 0000000..748ed3f --- /dev/null +++ b/frontend/src/pages/playground/Canvas.svelte @@ -0,0 +1,24 @@ + + + +
color: {color}
+redraws: {redraws}
diff --git a/lua/main.tl b/lua/main.tl index ec6ca82..ccbe073 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -2,7 +2,7 @@ local json = require("json") local fb = require("framebuffer") local ringbuffer = require("ringbuffer") local UUID = "8b9faf9f-9470-4a50-b405-0af5f0152550" -local ENDPOINT = "ws://localhost:8000/monitoring/computer/" .. UUID .. "/ws" +local ENDPOINT = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws" print("[MAIN] Init") diff --git a/server/dummy-client.py b/server/dummy-client.py new file mode 100644 index 0000000..d9cc40b --- /dev/null +++ b/server/dummy-client.py @@ -0,0 +1,96 @@ +import asyncio +import json +import websockets + +ENDPOINT = "ws://localhost:8000/ipmi/computer/8b9faf9f-9470-4a50-b405-0af5f0152550/ws" + + +def gen_payload(tick: int): + return { + "x": 3, + "y": 4, + "width": 39, + "height": 13, + "blink": True, + "fg": 0, + "text": [ + "[WS] OK\u0003 ", + "FG 0123456789ABCDEF ", + "BG 0123456789ABCDEF ", + " ", + f"Tick: {tick:8d} ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + "fg_color": [ + "78870dd50000000000000000000000000000000", + "0000123456789abcdef00000000000000000000", + "000f00000000000000000000000000000000000", + "440000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + "000000000000000000000000000000000000000", + ], + "bg_color": [ + "fffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffff0ffffffffffffffffffff", + "fff0123456789abcdefffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + "fffffffffffffffffffffffffffffffffffffff", + ], + "palette": [ + 15790320, + 15905331, + 15040472, + 10072818, + 14605932, + 8375321, + 15905484, + 5000268, + 10066329, + 5020082, + 11691749, + 3368652, + 8349260, + 5744206, + 13388876, + 1118481, + ], + } + + +async def main(): + tick = 0 + + async with websockets.connect(ENDPOINT) as socket: + while True: + await socket.send(json.dumps({"screen": gen_payload(tick)})) + await asyncio.sleep(1 / 20) + tick += 1 + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("this handler gets it") diff --git a/server/server/__init__.py b/server/server/__init__.py index 2b52354..7ae8c91 100644 --- a/server/server/__init__.py +++ b/server/server/__init__.py @@ -1,3 +1,4 @@ +import asyncio import json from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse @@ -6,14 +7,20 @@ from .settings import settings from .user import user_auth from .map_tiles import map_tiles, map_meta from .templates import j2env -from .monitoring import monitoring +from .monitoring import monitoring, ws_manager app = FastAPI() app.mount("/user/", user_auth) app.mount("/map/", map_meta) app.mount("/tiles/", map_tiles) -app.mount("/monitoring/", monitoring) +app.mount("/ipmi/", monitoring) + + +@app.on_event("startup") +async def on_startup(): + asyncio.get_running_loop().create_task(ws_manager.queue_task()) + frontend = FastAPI() diff --git a/server/server/monitoring.py b/server/server/monitoring.py index a55fd56..6801f58 100644 --- a/server/server/monitoring.py +++ b/server/server/monitoring.py @@ -1,10 +1,13 @@ +import asyncio import json from uuid import UUID from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError + monitoring = FastAPI() + class ScreenContent(BaseModel): x: int y: int @@ -15,24 +18,86 @@ class ScreenContent(BaseModel): text: list[str] fg_color: list[str] bg_color: list[str] + palette: list[int] -class Ping(BaseModel): - screen: ScreenContent -@monitoring.post("/ping") -async def ping(request: Request, data: Ping): - print("[PING]") - for line in data.screen.text: - print(line) +class Update(BaseModel): + screen: ScreenContent | None + + +class WSManager: + def __init__(self): + self.computers: dict[UUID, WebSocket] = dict() + self.viewers: dict[UUID, set[WebSocket]] = dict() + self.queue: asyncio.Queue[tuple[UUID, any]] = asyncio.Queue() + + async def queue_task(self): + print("[WS] queue task started") + while True: + (uuid, message) = await self.queue.get() + + if uuid not in self.viewers: + continue + + viewers = self.viewers[uuid] + await asyncio.gather(*(viewer.send_json(message) for viewer in viewers)) + + async def broadcast(self, uuid: UUID, message): + await self.queue.put((uuid, message)) + + async def on_computer_connect(self, socket: WebSocket, uuid: UUID): + if uuid in self.computers: + print(f"[WS] Closing duplicate connection for {uuid}") + await socket.close() + return + + print(f"[WS] Computer {uuid} connected") + self.computers[uuid] = socket + while True: + try: + data = await socket.receive_json() + data = Update.parse_obj(data) + + if data.screen: + await self.broadcast(uuid, data.screen.dict()) + + except ValidationError as e: + print(f"[WS] Received invalid message from {uuid}:") + print(e.json) + except WebSocketDisconnect: + break + + del self.computers[uuid] + print(f"[WS] Computer {uuid} disconnected") + + async def on_browser_connect(self, socket: WebSocket, uuid: UUID): + print(f"[WS] Browser connected for {uuid}") + + if uuid not in self.viewers: + self.viewers[uuid] = set() + + self.viewers[uuid].add(socket) + + while True: + try: + data = await socket.receive_json() + except WebSocketDisconnect: + break + self.viewers[uuid].remove(socket) + print(f"[WS] Browser disconnected for {uuid}") + + +ws_manager = WSManager() + + @monitoring.websocket("/computer/{uuid}/ws") async def computer_ws(socket: WebSocket, uuid: UUID): await socket.accept() - print(f"[WS] Computer {uuid} connected") - while True: - try: - data = await socket.receive_json() - #print(f"[WS] rx {json.dumps(data)}") - except WebSocketDisconnect: - break - print(f"[WS] Computer {uuid} disconnected") \ No newline at end of file + await ws_manager.on_computer_connect(socket, uuid) + + +@monitoring.websocket("/browser/{uuid}/ws") +async def browser_ws(socket: WebSocket, uuid: UUID): + await socket.accept() + await ws_manager.on_browser_connect(socket, uuid) From 226aba437ad98e51dab016c12a01e93e0a5cb2a9 Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Thu, 22 Sep 2022 21:01:40 +0200 Subject: [PATCH 4/9] Implement /install --- lua/.gitignore | 1 + lua/justfile | 19 +++++++++++ lua/main.tl | 61 +++++++++++++++++++++++++++++------- server/lua | 1 + server/server/__init__.py | 15 +++++++-- server/server/settings.py | 3 ++ server/templates/install.lua | 6 ++++ 7 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 lua/.gitignore create mode 100644 lua/justfile create mode 120000 server/lua create mode 100644 server/templates/install.lua diff --git a/lua/.gitignore b/lua/.gitignore new file mode 100644 index 0000000..c585e19 --- /dev/null +++ b/lua/.gitignore @@ -0,0 +1 @@ +out \ No newline at end of file diff --git a/lua/justfile b/lua/justfile new file mode 100644 index 0000000..3056042 --- /dev/null +++ b/lua/justfile @@ -0,0 +1,19 @@ +default: + @just --list + +teal_files := "main.tl framebuffer.tl ringbuffer.tl" + +build: + mkdir -p out + cp json.lua out + 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 \ No newline at end of file diff --git a/lua/main.tl b/lua/main.tl index ccbe073..5fa7641 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -8,17 +8,40 @@ print("[MAIN] Init") local enum SocketState "reset" - "connecting" -- currently unused + "error" "ok" + "viewer_connected" end +local BAD_STATES : {SocketState: boolean} = { + ["reset"] = true, + ["error"] = true, +} + +local type SocketStateCallback = function(new_state: SocketState) + local record Socket state: SocketState + bad_state: function(self: Socket): boolean + set_state: function(self: Socket, state: SocketState) + on_state_change: function(self: Socket, cb: SocketStateCallback) + _callback: SocketStateCallback ws: http.Websocket end local socket: Socket = { state = "reset", + bad_state = function(self: Socket): boolean + return BAD_STATES[self.state] ~= nil + end, + set_state = function(self: Socket, state: SocketState) + self.state = state + self._callback(state) + end, + on_state_change = function(self: Socket, cb: SocketStateCallback) + self._callback = cb + end, + _callback = function(_: SocketState) end, ws = nil, } @@ -26,13 +49,13 @@ local function send(message: string) -- "message" needs to be valid JSON -- otherwise the server will not accept it - if socket.state ~= "ok" then return end + if socket:bad_state() then return end local r = { pcall(socket.ws.send, message) } if r[1] == false then if (r[2] as string):sub(-11) == "closed file" then - socket.state = "reset" + socket:set_state("reset") elseif (r[2] as string):sub(-9) == "too large" then -- TODO handle -- the connection stays open though @@ -66,22 +89,32 @@ end -- Create tasks +local bar_codes: { SocketState: {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: SocketState) + set_bar(table.unpack(bar_codes[new_state])) +end) + local ws_task = coroutine.create(function() while true do - if socket.state == "reset" then - set_bar("[WS] RST", "78870111") + if socket:bad_state() then + --set_bar("[WS] RST", "78870111") local r = http.websocket(ENDPOINT) if r ~= false then socket.ws = r as http.Websocket - set_bar("[WS] OK\x03", "78870DD5") - socket.state = "ok" + --set_bar("[WS] OK\x03", "78870DD5") + socket:set_state("ok") else - set_bar("[WS] ERR", "78870EEE") + socket:set_state("error") + --set_bar("[WS] ERR", "78870EEE") end end - repeat - sleep(1) - until socket.state ~= "ok" + sleep(1) end end) @@ -89,7 +122,8 @@ local report_task = coroutine.create(function() local last_report = -1.0 while true do local now = os.clock() - if now - last_report >= 0.05 then + local interval = (socket.state == "viewer_connected") and 0.05 or 1 + if now - last_report >= interval then local message = json.encode({ screen = buffer.serialize() }) @@ -151,6 +185,9 @@ while shell_running do end end +if socket.state == "ok" then + socket.ws.close() +end term.native = orig_native term.redirect(term.native()) diff --git a/server/lua b/server/lua new file mode 120000 index 0000000..df6ec1a --- /dev/null +++ b/server/lua @@ -0,0 +1 @@ +../lua/out \ No newline at end of file diff --git a/server/server/__init__.py b/server/server/__init__.py index 7ae8c91..e044d6c 100644 --- a/server/server/__init__.py +++ b/server/server/__init__.py @@ -1,7 +1,8 @@ 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 @@ -16,6 +17,16 @@ 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(): @@ -26,8 +37,6 @@ 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) diff --git a/server/server/settings.py b/server/server/settings.py index 6191c93..6efdc02 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -7,6 +7,9 @@ class Settings(BaseSettings): frontend_path: str = "frontend" unmined_out_path: str = "unmined-out" + lua_out_path: str = "lua" + + deploy_path: str = "http://localhost:8000" settings = Settings() diff --git a/server/templates/install.lua b/server/templates/install.lua new file mode 100644 index 0000000..6905f89 --- /dev/null +++ b/server/templates/install.lua @@ -0,0 +1,6 @@ +local path = "{{ deploy_path }}" +files = { "main.lua", "json.lua", "framebuffer.lua", "ringbuffer.lua" } +for _, file in ipairs(files) do + fs.delete(file) + shell.run(("wget %s/lua/%s"):format(path, file)) +end \ No newline at end of file From fb22cc75282510b948cec2bd5b8366facdb11a8b Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Thu, 22 Sep 2022 21:32:31 +0200 Subject: [PATCH 5/9] Refactor socket into socket.lua --- lua/framebuffer.tl | 2 +- lua/justfile | 2 +- lua/main.tl | 89 +++++------------------------------- lua/ringbuffer.tl | 3 +- lua/socket.tl | 85 ++++++++++++++++++++++++++++++++++ server/templates/install.lua | 2 +- 6 files changed, 102 insertions(+), 81 deletions(-) create mode 100644 lua/socket.tl diff --git a/lua/framebuffer.tl b/lua/framebuffer.tl index 18edb00..94fc03d 100644 --- a/lua/framebuffer.tl +++ b/lua/framebuffer.tl @@ -136,4 +136,4 @@ local function wrap(parent: term.Redirect): Buffer end -return { wrap = wrap } \ No newline at end of file +return { wrap = wrap, Buffer = Buffer, ScreenContent = ScreenContent } \ No newline at end of file diff --git a/lua/justfile b/lua/justfile index 3056042..acf094b 100644 --- a/lua/justfile +++ b/lua/justfile @@ -1,7 +1,7 @@ default: @just --list -teal_files := "main.tl framebuffer.tl ringbuffer.tl" +teal_files := "main.tl framebuffer.tl ringbuffer.tl socket.tl" build: mkdir -p out diff --git a/lua/main.tl b/lua/main.tl index 5fa7641..f26992f 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -1,74 +1,20 @@ local json = require("json") -local fb = require("framebuffer") -local ringbuffer = require("ringbuffer") +local Framebuffer = require("framebuffer") +local Ringbuffer = require("ringbuffer") +local Socket = require("socket") local UUID = "8b9faf9f-9470-4a50-b405-0af5f0152550" local ENDPOINT = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws" print("[MAIN] Init") -local enum SocketState - "reset" - "error" - "ok" - "viewer_connected" -end - -local BAD_STATES : {SocketState: boolean} = { - ["reset"] = true, - ["error"] = true, -} - -local type SocketStateCallback = function(new_state: SocketState) - -local record Socket - state: SocketState - bad_state: function(self: Socket): boolean - set_state: function(self: Socket, state: SocketState) - on_state_change: function(self: Socket, cb: SocketStateCallback) - _callback: SocketStateCallback - ws: http.Websocket -end - -local socket: Socket = { - state = "reset", - bad_state = function(self: Socket): boolean - return BAD_STATES[self.state] ~= nil - end, - set_state = function(self: Socket, state: SocketState) - self.state = state - self._callback(state) - end, - on_state_change = function(self: Socket, cb: SocketStateCallback) - self._callback = cb - end, - _callback = function(_: SocketState) end, - ws = nil, -} - -local function send(message: string) - -- "message" needs to be valid JSON - -- otherwise the server will not accept it - - if socket:bad_state() then return end - - local r = { pcall(socket.ws.send, message) } - - if r[1] == false then - if (r[2] as string):sub(-11) == "closed file" then - socket:set_state("reset") - elseif (r[2] as string):sub(-9) == "too large" then - -- TODO handle - -- the connection stays open though - end - end -end +local socket = Socket.new(ENDPOINT) -- Set up framebuffer capture and statusline print("[MAIN] Setup framebuffer") local orig_native = term.native -local buffer = fb.wrap(orig_native()) +local buffer = Framebuffer.wrap(orig_native()) term.native = function(): term.Redirect return buffer.target end @@ -89,30 +35,21 @@ end -- Create tasks -local bar_codes: { SocketState: {string} } = { +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: SocketState) +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:bad_state() then - --set_bar("[WS] RST", "78870111") - local r = http.websocket(ENDPOINT) - if r ~= false then - socket.ws = r as http.Websocket - --set_bar("[WS] OK\x03", "78870DD5") - socket:set_state("ok") - else - socket:set_state("error") - --set_bar("[WS] ERR", "78870EEE") - end + if socket:is_bad_state() then + socket:reconnect() end sleep(1) end @@ -127,7 +64,7 @@ local report_task = coroutine.create(function() local message = json.encode({ screen = buffer.serialize() }) - send(message) + socket:send(message) last_report = now end sleep(0) -- until next gametick @@ -151,7 +88,7 @@ local tasks: {Task} = { {coro = report_task}, } -local event_queue = ringbuffer.new(64) +local event_queue: Ringbuffer.Ringbuffer = Ringbuffer.new(64) event_queue:push({n = 0}) local shell_running = true @@ -185,9 +122,7 @@ while shell_running do end end -if socket.state == "ok" then - socket.ws.close() -end +socket:close() term.native = orig_native term.redirect(term.native()) diff --git a/lua/ringbuffer.tl b/lua/ringbuffer.tl index ca7afe0..fb8c3e1 100644 --- a/lua/ringbuffer.tl +++ b/lua/ringbuffer.tl @@ -37,5 +37,6 @@ local function new(size: integer): Ringbuffer end return { - new = new + new = new, + Ringbuffer = Ringbuffer } \ No newline at end of file diff --git a/lua/socket.tl b/lua/socket.tl new file mode 100644 index 0000000..7c86f21 --- /dev/null +++ b/lua/socket.tl @@ -0,0 +1,85 @@ +local enum State + "reset" + "error" + "ok" + "viewer_connected" +end + +local BAD_STATES : {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) + _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 + +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 } \ No newline at end of file diff --git a/server/templates/install.lua b/server/templates/install.lua index 6905f89..9f0d87b 100644 --- a/server/templates/install.lua +++ b/server/templates/install.lua @@ -1,5 +1,5 @@ local path = "{{ deploy_path }}" -files = { "main.lua", "json.lua", "framebuffer.lua", "ringbuffer.lua" } +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)) From d1df4ff6a212087d4393f65c70c3bfc7514e1f65 Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Fri, 23 Sep 2022 00:31:18 +0200 Subject: [PATCH 6/9] Implement viewer_{dis,}connect events --- lua/main.tl | 48 +++++++++++++++++++++++-------------- lua/socket.tl | 7 ++++++ server/server/monitoring.py | 24 ++++++++++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/lua/main.tl b/lua/main.tl index f26992f..1929f5c 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -13,6 +13,7 @@ local socket = Socket.new(ENDPOINT) 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 @@ -100,23 +101,34 @@ while shell_running do else e = table.pack(os.pullEventRaw()) end - - for pid = 1, #tasks do - local task = tasks[pid] - if task.filter == nil or task.filter == e[1] or e[1] == "terminate" then - local ok, param = coroutine.resume(task.coro, table.unpack(e as {any})) - if not ok then - term.redirect(orig_native()) - term.clear() - term.setCursorPos(1,1) - print("OMEGABIG OOF") - print(("pid %d"):format(pid)) - error(param, 0) - else - task.filter = param as string - end - if pid == 1 and coroutine.status(task.coro) == "dead" then - shell_running = false + + 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, #tasks do + local task = tasks[pid] + if task.filter == nil or task.filter == e[1] or e[1] == "terminate" then + local ok, param = coroutine.resume(task.coro, table.unpack(e as {any})) + if not ok then + term.redirect(orig_native()) + term.clear() + term.setCursorPos(1,1) + print("OMEGABIG OOF") + print(("pid %d"):format(pid)) + error(param, 0) + else + task.filter = param as string + end + if pid == 1 and coroutine.status(task.coro) == "dead" then + shell_running = false + end end end end @@ -125,6 +137,6 @@ end socket:close() term.native = orig_native -term.redirect(term.native()) +term.redirect(prev_term) term.clear() term.setCursorPos(1,1) \ No newline at end of file diff --git a/lua/socket.tl b/lua/socket.tl index 7c86f21..7ecc127 100644 --- a/lua/socket.tl +++ b/lua/socket.tl @@ -20,6 +20,7 @@ local record Socket 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 @@ -73,6 +74,12 @@ impl.close = function(self: Socket) 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", diff --git a/server/server/monitoring.py b/server/server/monitoring.py index 6801f58..0f9ea11 100644 --- a/server/server/monitoring.py +++ b/server/server/monitoring.py @@ -31,6 +31,18 @@ class WSManager: 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: @@ -53,6 +65,10 @@ class WSManager: 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() @@ -76,6 +92,9 @@ class WSManager: 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: @@ -83,8 +102,11 @@ class WSManager: 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}") From 36427a41a4742659d1e2aa68289c5a6540e0d818 Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Sun, 25 Sep 2022 11:47:01 +0200 Subject: [PATCH 7/9] Refactor main loop --- lua/main.tl | 70 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/lua/main.tl b/lua/main.tl index 1929f5c..04fd9cb 100644 --- a/lua/main.tl +++ b/lua/main.tl @@ -72,10 +72,6 @@ local report_task = coroutine.create(function() end end) -local shell_task = coroutine.create(function() - shell.run("shell") -end) - -- basically parallel.waitForAny local record Task @@ -83,15 +79,39 @@ local record Task filter: string | nil end -local tasks: {Task} = { - {coro = shell_task}, -- pid 1 +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
= Ringbuffer.new(64) event_queue:push({n = 0}) +local shell_deaths: {any} = {} + local shell_running = true while shell_running do local e: table @@ -112,25 +132,23 @@ while shell_running do socket:signal_viewer_connect(false) end else - for pid = 1, #tasks do - local task = tasks[pid] - if task.filter == nil or task.filter == e[1] or e[1] == "terminate" then - local ok, param = coroutine.resume(task.coro, table.unpack(e as {any})) - if not ok then - term.redirect(orig_native()) - term.clear() - term.setCursorPos(1,1) - print("OMEGABIG OOF") - print(("pid %d"):format(pid)) - error(param, 0) - else - task.filter = param as string - end - if pid == 1 and coroutine.status(task.coro) == "dead" then - shell_running = false - end + 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 @@ -139,4 +157,8 @@ socket:close() term.native = orig_native term.redirect(prev_term) term.clear() -term.setCursorPos(1,1) \ No newline at end of file +term.setCursorPos(1,1) + +for i = 1, #shell_deaths do + print(shell_deaths[i]) +end \ No newline at end of file From 2726baa6bd38425abea1951c6fe922de05a87f6b Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Sun, 25 Sep 2022 11:47:15 +0200 Subject: [PATCH 8/9] Implement justfile watch mode --- lua/justfile | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lua/justfile b/lua/justfile index acf094b..cfe7f6d 100644 --- a/lua/justfile +++ b/lua/justfile @@ -1,11 +1,14 @@ default: @just --list -teal_files := "main.tl framebuffer.tl ringbuffer.tl socket.tl" +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 - cp json.lua 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; \ @@ -16,4 +19,12 @@ alias b := build clean: rm -r out -alias c := clean \ No newline at end of file +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 \ No newline at end of file From 1fd741861eb16938b915c8ecec399f620e409da7 Mon Sep 17 00:00:00 2001 From: Kai Vogelgesang Date: Sun, 25 Sep 2022 14:40:07 +0200 Subject: [PATCH 9/9] Implement database connection --- db/.gitignore | 1 + db/docker-compose.yml | 19 ++++++ db/secret.env.example | 5 ++ server/.gitignore | 1 + server/README.md | 6 +- server/db.env.example | 1 + server/poetry.lock | 112 +++++++++++++++++++++++++++++++++++- server/pyproject.toml | 1 + server/server/db.py | 24 ++++++++ server/server/monitoring.py | 56 +++++++++++++++++- server/server/settings.py | 2 + 11 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 db/.gitignore create mode 100644 db/docker-compose.yml create mode 100644 db/secret.env.example create mode 100644 server/db.env.example create mode 100644 server/server/db.py diff --git a/db/.gitignore b/db/.gitignore new file mode 100644 index 0000000..5ebf0ae --- /dev/null +++ b/db/.gitignore @@ -0,0 +1 @@ +secret.env diff --git a/db/docker-compose.yml b/db/docker-compose.yml new file mode 100644 index 0000000..5f96734 --- /dev/null +++ b/db/docker-compose.yml @@ -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 diff --git a/db/secret.env.example b/db/secret.env.example new file mode 100644 index 0000000..77092e7 --- /dev/null +++ b/db/secret.env.example @@ -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/" diff --git a/server/.gitignore b/server/.gitignore index 20471dc..4999ca2 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -2,3 +2,4 @@ secret.env unmined-out **/__pycache__ +db.env diff --git a/server/README.md b/server/README.md index 97dd0c0..feba9c3 100644 --- a/server/README.md +++ b/server/README.md @@ -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 -``` \ No newline at end of file +$ poetry run uvicorn server:app --env-file db.env +``` diff --git a/server/db.env.example b/server/db.env.example new file mode 100644 index 0000000..67fad12 --- /dev/null +++ b/server/db.env.example @@ -0,0 +1 @@ +MONGO_URI="mongodb://user:pass@localhost:27017/" diff --git a/server/poetry.lock b/server/poetry.lock index 4206215..d728a8f 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -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 " +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"}, diff --git a/server/pyproject.toml b/server/pyproject.toml index ad35bdf..9207fe4 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -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] diff --git a/server/server/db.py b/server/server/db.py new file mode 100644 index 0000000..bef9abe --- /dev/null +++ b/server/server/db.py @@ -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") diff --git a/server/server/monitoring.py b/server/server/monitoring.py index 0f9ea11..9c8ef40 100644 --- a/server/server/monitoring.py +++ b/server/server/monitoring.py @@ -1,9 +1,21 @@ import asyncio -import json +from typing import Any from uuid import UUID +from datetime import datetime +from bson import ObjectId -from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect -from pydantic import BaseModel, ValidationError +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() @@ -123,3 +135,41 @@ async def computer_ws(socket: WebSocket, uuid: UUID): 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 diff --git a/server/server/settings.py b/server/server/settings.py index 6efdc02..348860a 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -9,6 +9,8 @@ class Settings(BaseSettings): unmined_out_path: str = "unmined-out" lua_out_path: str = "lua" + mongo_uri: str + deploy_path: str = "http://localhost:8000"