potatOS/src/polychoron.lua

295 lines
8.1 KiB
Lua

local version = "1.6"
-- Localize frequently used functions for performance
local osepoch = os.epoch
local osclock = os.clock
local stringformat = string.format
local coroutineresume = coroutine.resume
local coroutineyield = coroutine.yield
local coroutinestatus = coroutine.status
local tostring = tostring
local ccemuxnanoTime
if ccemux then
ccemuxnanoTime = ccemux.nanoTime
end
-- Return a time of some sort. Not used to provide "objective" time measurement, just for duration comparison
local function time()
if ccemuxnanoTime then
return ccemuxnanoTime() / 1e9
elseif osepoch then
return osepoch "utc" / 1000 else
return os.clock() end
end
local processes = {}
_G.process = {}
-- Allow getting processes by name, and nice process views from process.list()
local process_list_mt = {
__tostring = function(ps)
local o = ""
for _, p in pairs(ps) do
o = o .. tostring(p)
o = o .. "\n"
end
return o:gsub("\n$", "") -- strip trailing newline
end,
__index = function(tabl, key)
for i, p in pairs(tabl) do
if p.name == key then return p end
end
end
}
setmetatable(processes, process_list_mt)
-- To make suspend kind of work with sleep, we need to bodge it a bit
-- So this modified sleep *also* checks the time, in case timer events were eaten
function _G.sleep(time)
time = time or 0
local t = os.startTimer(time)
local start = os.clock()
local ev, arg, tdiff
repeat
ev, arg = os.pullEvent()
until (ev == "timer" and arg == t) or (os.clock() - start) > time
end
process.statuses = {
DEAD = "dead",
ERRORED = "errored",
OK = "ok",
STOPPED = "stopped"
}
process.signals = {
START = "start",
STOP = "stop",
TERMINATE = "terminate",
KILL = "kill"
}
-- Gets the first key in a table with the given value
local function get_key_with_value(t, v)
for tk, tv in pairs(t) do
if v == tv then
return tk
end
end
end
-- Contains custom stringification, and an equality thing using IDs
local process_metatable = {
__tostring = function(p)
local text = stringformat("[process %d %s: %s", p.ID, p.name or "[unnamed]", get_key_with_value(process.statuses, p.status) or "?")
if p.parent then
text = text .. stringformat("; parent %s", p.parent.name or p.parent.ID)
end
return text .. "]"
end,
__eq = function(p1, p2)
return p1.ID == p2.ID
end
}
-- Whitelist of events which ignore filters.
local allow_event = {
terminate = true
}
local function process_to_info(p)
if not p then return nil end
local out = {}
for k, v in pairs(p) do
if k == "parent" and v ~= nil then
out.parent = process_to_info(v)
else
-- PS#85DD8AFC
-- Through some bizarre environment weirdness even exposing the function causes security risks. So don't.
if k ~= "coroutine" and k ~= "function" then
out[k] = v
end
end
end
setmetatable(out, process_metatable)
return out
end
-- Fancy BSOD
local function BSOD(e)
if _G.add_log then _G.add_log("BSOD recorded: %s", e) end
if term.isColor() then term.setBackgroundColor(colors.blue) term.setTextColor(colors.white)
else term.setBackgroundColor(colors.white) term.setTextColor(colors.black) end
term.clear()
term.setCursorBlink(false)
term.setCursorPos(1, 1)
print(e)
end
local running
-- Apply "event" to "proc"
-- Where most important stuff happens
local function tick(proc, event)
if not proc then error "No such process" end
if process.running and process.running.ID == proc.ID then return end
-- Run any given event preprocessor on the event
-- Actually don't, due to (hypothetical) PS#D7CD76C0-like exploits
--[[
if type(proc.event_preprocessor) == "function" then
event = proc.event_preprocessor(event)
if event == nil then return end
end
]]
-- If coroutine is dead, just ignore it but set its status to dead
if coroutinestatus(proc.coroutine) == "dead" then
proc.status = process.statuses.DEAD
if proc.ephemeral then
processes[proc.ID] = nil
end
end
-- If coroutine ready and filter matches or event is allowed, run it, set the running process in its environment,
-- get execution time, and run error handler if errors happen.
if proc.status == process.statuses.OK and (proc.filter == nil or proc.filter == event[1] or allow_event[event[1]]) then
process.running = process_to_info(proc)
running = proc
local start_time = time()
local ok, res = coroutineresume(proc.coroutine, table.unpack(event))
local end_time = time()
proc.execution_time = end_time - start_time
if not ok then
if proc.error_handler then
proc.error_handler(res)
else
proc.status = process.statuses.ERRORED
proc.error = res
if res ~= "Terminated" then -- programs terminating is normal, other errors not so much
BSOD(stringformat("Process %s has crashed!\nError: %s", tostring(proc.ID) or proc.name, tostring(res)))
end
end
else
proc.filter = res
end
process.running = nil
end
end
function process.get_running()
return running
end
-- Send/apply the given signal to the given process
local function apply_signal(proc, signal)
local rID = nil
if process.running then rID = process.running.ID end
tick(proc, { "signal", signal, rID })
-- START - starts stopped process
if signal == process.signals.START and proc.status == process.statuses.STOPPED then
proc.status = process.statuses.OK
-- STOP stops started process
elseif signal == process.signals.STOP and proc.status == process.statuses.OK then
proc.status = process.statuses.STOPPED
elseif signal == process.signals.TERMINATE then
proc.terminated_time = os.clock()
tick(proc, { "terminate" })
elseif signal == process.signals.KILL then
proc.status = process.statuses.DEAD
end
end
local next_ID = 1
function process.spawn(fn, name, extra)
local this_ID = next_ID
local proc = {
coroutine = coroutine.create(fn),
name = name,
status = process.statuses.OK,
ID = this_ID,
parent = process.running,
["function"] = fn
}
if extra then for k, v in pairs(extra) do proc[k] = v end end
setmetatable(proc, process_metatable)
processes[this_ID] = proc
next_ID = next_ID + 1
return this_ID
end
function process.thread(fn, name)
local parent = process.running.name or tostring(process.running.ID)
process.spawn(fn, ("%s_%s_%04x"):format(name or "thread", parent, math.random(0, 0xFFFF)), { ephemeral = true })
end
-- Sends a signal to the given process ID
function process.signal(ID, signal)
if not processes[ID] then error(stringformat("No such process %s.", tostring(ID))) end
apply_signal(processes[ID], signal)
end
-- PS#F7686798
-- Prevent mutation of processes through exposed API to prevent PS#D7CD76C0-like exploits
-- List all processes
function process.list()
local out = {}
for k, v in pairs(processes) do
out[k] = process_to_info(v)
end
return setmetatable(out, process_list_mt)
end
function process.info(ID)
return process_to_info(processes[ID])
end
-- Run main event loop
local function run_loop()
while true do
local ev = {coroutineyield()}
for ID, proc in pairs(processes) do
tick(proc, ev)
end
end
end
local base_processes = {
["main"] = function() os.run({}, "autorun.lua") end,
["rednetd"] = function()
-- bodge, because of the stupid rednet bRunning thing
local old_error = error
_G.error = function() _G.error = old_error end
rednet.run()
end
}
-- hacky magic to run our code and not the BIOS stuff
-- this terminates the shell, which crashes the BIOS, which then causes an error, which is printed with printError
local old_printError = _G.printError
function _G.printError()
_G.printError = old_printError
-- Multishell must die.
term.redirect(term.native())
multishell = nil
term.setTextColor(colors.yellow)
term.setBackgroundColor(colors.black)
term.setCursorPos(1,1)
term.clear()
_G.polychoron = {version = version, process = process}
polychoron.polychoron = polychoron
polychoron.BSOD = BSOD
for n, p in pairs(base_processes) do
process.spawn(p, n)
end
os.queueEvent "event" -- so that processes get one free "tick"
run_loop()
end
os.queueEvent "terminate"