195 lines
5.1 KiB
Plaintext
195 lines
5.1 KiB
Plaintext
local json = require("json")
|
|
local fb = require("framebuffer")
|
|
local ringbuffer = require("ringbuffer")
|
|
local UUID <const> = "8b9faf9f-9470-4a50-b405-0af5f0152550"
|
|
local ENDPOINT <const> = "ws://localhost:8000/ipmi/computer/" .. UUID .. "/ws"
|
|
|
|
print("[MAIN] Init")
|
|
|
|
local enum SocketState
|
|
"reset"
|
|
"error"
|
|
"ok"
|
|
"viewer_connected"
|
|
end
|
|
|
|
local BAD_STATES <const> : {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
|
|
|
|
-- 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
|
|
|
|
local width, height = term.getSize()
|
|
local top_line = window.create(buffer.target, 1, 1, width, 1)
|
|
local main_view = window.create(buffer.target, 1, 2, width, height - 1)
|
|
term.redirect(main_view as term.Redirect)
|
|
|
|
local function set_bar(text: string, fg: string | nil, bg: string | nil)
|
|
fg = fg or ("9"):rep(text:len())
|
|
bg = bg or ("f"):rep(text:len())
|
|
top_line.clear()
|
|
top_line.setCursorPos(1,1)
|
|
top_line.blit(text, fg, bg)
|
|
main_view.restoreCursor()
|
|
end
|
|
|
|
-- Create tasks
|
|
|
|
local bar_codes: { 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: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
|
|
end
|
|
sleep(1)
|
|
end
|
|
end)
|
|
|
|
local report_task = coroutine.create(function()
|
|
local last_report = -1.0
|
|
while true do
|
|
local now = os.clock()
|
|
local interval = (socket.state == "viewer_connected") and 0.05 or 1
|
|
if now - last_report >= interval then
|
|
local message = json.encode({
|
|
screen = buffer.serialize()
|
|
})
|
|
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
|
|
|
|
if socket.state == "ok" then
|
|
socket.ws.close()
|
|
end
|
|
|
|
term.native = orig_native
|
|
term.redirect(term.native())
|
|
term.clear()
|
|
term.setCursorPos(1,1) |