476 lines
13 KiB
Lua
476 lines
13 KiB
Lua
local DEBUG_MODE = settings.get "potatOS.polychoron_debug"
|
|
|
|
-- 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 coroutinecreate = coroutine.create
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local setmetatable = setmetatable
|
|
local tableinsert = table.insert
|
|
local assert = assert
|
|
local error = error
|
|
local tableunpack = table.unpack
|
|
local debugtraceback = debug and debug.traceback
|
|
local osqueueevent = os.queueEvent
|
|
local ccemuxnanoTime
|
|
local ccemuxecho
|
|
if ccemux then
|
|
ccemuxnanoTime = ccemux.nanoTime
|
|
ccemuxecho = ccemux.echo
|
|
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 osclock() end
|
|
end
|
|
|
|
local processes = {}
|
|
_G.process = {}
|
|
|
|
local function copy(t)
|
|
local out = {}
|
|
for k, v in pairs(t) do
|
|
out[k] = v
|
|
end
|
|
return out
|
|
end
|
|
|
|
local statuses = {
|
|
DEAD = "dead",
|
|
ERRORED = "errored",
|
|
OK = "ok",
|
|
STOPPED = "stopped"
|
|
}
|
|
process.statuses = copy(statuses)
|
|
|
|
local signals = {
|
|
START = "start",
|
|
STOP = "stop",
|
|
TERMINATE = "terminate",
|
|
KILL = "kill"
|
|
}
|
|
process.signals = copy(signals)
|
|
|
|
-- 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)
|
|
if type(key) == "table" and key.ID then return tabl[key.ID] end
|
|
for i, p in pairs(tabl) do
|
|
if p.name == key and p.status ~= statuses.DEAD then return p end
|
|
end
|
|
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 = osclock()
|
|
local ev, arg, tdiff
|
|
|
|
repeat
|
|
ev, arg = os.pullEvent()
|
|
until (ev == "timer" and arg == t) or (osclock() - start) > time
|
|
end
|
|
|
|
-- 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 DEBUG_MODE then return p end
|
|
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)
|
|
elseif k == "thread_parent" and v ~= nil then
|
|
out.thread_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" and k ~= "table" then
|
|
out[k] = v
|
|
end
|
|
end
|
|
end
|
|
out.capabilities = { restrictions = copy(p.capabilities.restrictions), grants = copy(p.capabilities.grants) }
|
|
setmetatable(out, process_metatable)
|
|
return out
|
|
end
|
|
|
|
-- Fancy BSOD
|
|
local function BSOD(e)
|
|
if false then
|
|
|
|
if _G.add_log then _G.add_log("failure recorded: %s", e) end
|
|
if _G.add_log and debugtraceback then _G.add_log("stack traceback: %s", debugtraceback()) 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
|
|
end
|
|
|
|
local running
|
|
-- Apply "event" to "proc"
|
|
-- Where most important stuff happens
|
|
local function tick(proc, event)
|
|
if not proc then error "Internal error: No such process" end
|
|
if running 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 and set its status to dead
|
|
if coroutinestatus(proc.coroutine) == "dead" or proc.status == statuses.DEAD then
|
|
proc.status = statuses.DEAD
|
|
if proc.thread then processes[proc.ID] = nil end
|
|
return
|
|
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 == statuses.OK and (proc.filter == nil or proc.filter == event[1] or (type(proc.filter) == "table" and 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
|
|
proc.ctime = proc.ctime + end_time - start_time
|
|
if not ok then
|
|
if proc.error_handler then
|
|
proc.error_handler(res)
|
|
else
|
|
proc.status = 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", proc.name or tostring(proc.ID), tostring(res)))
|
|
end
|
|
end
|
|
else
|
|
proc.filter = res
|
|
end
|
|
running = nil
|
|
process.running = nil
|
|
end
|
|
end
|
|
|
|
local queue = {}
|
|
local events_are_queued = false
|
|
|
|
local function find_all_in_group(id)
|
|
local proc = processes[id]
|
|
if proc.thread then
|
|
proc = proc.thread_parent
|
|
end
|
|
local procs = {proc}
|
|
for _, p in pairs(processes) do
|
|
if p.thread_parent == proc then
|
|
tableinsert(procs, p)
|
|
end
|
|
end
|
|
return procs
|
|
end
|
|
|
|
local function enqueue(id, event)
|
|
events_are_queued = true
|
|
for _, tg in pairs(find_all_in_group(id)) do
|
|
local id = tg.ID
|
|
queue[id] = queue[id] or {}
|
|
tableinsert(queue[id], event)
|
|
end
|
|
end
|
|
|
|
function process.get_running()
|
|
return process_to_info(running)
|
|
end
|
|
|
|
function process.IPC(target, ...)
|
|
if not processes[target] then error(stringformat("No such process %s.", tostring(target))) end
|
|
enqueue(processes[target].ID, { "ipc", running.ID, ... })
|
|
end
|
|
|
|
-- Send/apply the given signal to the given process
|
|
local function apply_signal(proc, signal)
|
|
enqueue(proc.ID, { "signal", signal, running.ID })
|
|
if signal == signals.TERMINATE then
|
|
enqueue(proc.ID, { "terminate" })
|
|
end
|
|
for _, proc in pairs(find_all_in_group(proc.ID)) do
|
|
-- START - starts stopped process
|
|
if signal == signals.START and proc.status == statuses.STOPPED then
|
|
proc.status = statuses.OK
|
|
-- STOP stops started process
|
|
elseif signal == signals.STOP and proc.status == statuses.OK then
|
|
proc.status = statuses.STOPPED
|
|
elseif signal == signals.TERMINATE then
|
|
proc.terminated_time = osclock()
|
|
elseif signal == signals.KILL then
|
|
proc.status = statuses.DEAD
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ensure_no_metatables(x)
|
|
if type(x) ~= "table" then return end
|
|
assert(getmetatable(x) == nil)
|
|
for k, v in pairs(x) do
|
|
ensure_no_metatables(v)
|
|
ensure_no_metatables(k)
|
|
end
|
|
end
|
|
|
|
local root_capability = {"root"}
|
|
|
|
local function ensure_capabilities_subset(x, orig)
|
|
x.grants = x.grants or {}
|
|
x.restrictions = x.restrictions or {}
|
|
ensure_no_metatables(x)
|
|
assert(type(x.restrictions) == "table")
|
|
assert(type(x.grants) == "table")
|
|
if orig.grants[root_capability] then return end
|
|
for restriction, value in pairs(orig.restrictions) do
|
|
x.restrictions[restriction] = value
|
|
end
|
|
for grant, enabled in pairs(x.grants) do
|
|
if enabled and not orig.grants[grant] then
|
|
x.grants[grant] = false
|
|
end
|
|
end
|
|
end
|
|
|
|
local function are_capabilities_subset(x, orig)
|
|
if orig.grants[root_capability] then return true end
|
|
for restriction, value in pairs(orig.restrictions) do
|
|
if x.restrictions[restriction] ~= value then
|
|
return false
|
|
end
|
|
end
|
|
for grant, enabled in pairs(x.grants) do
|
|
if enabled and not orig.grants[grant] then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local next_ID = 1
|
|
local function spawn(fn, name, thread, capabilities)
|
|
name = tostring(name)
|
|
local this_ID = next_ID
|
|
if not capabilities then
|
|
capabilities = running.capabilities
|
|
end
|
|
if running then ensure_capabilities_subset(capabilities, running.capabilities) end
|
|
local proc = {
|
|
coroutine = coroutinecreate(fn),
|
|
name = name,
|
|
status = statuses.OK,
|
|
ID = this_ID,
|
|
parent = running,
|
|
["function"] = fn,
|
|
ctime = 0,
|
|
capabilities = capabilities
|
|
}
|
|
|
|
if thread then
|
|
proc.thread_parent = running.thread_parent or running
|
|
proc.thread = true
|
|
proc.parent = running.parent
|
|
end
|
|
|
|
setmetatable(proc, process_metatable)
|
|
processes[this_ID] = proc
|
|
next_ID = next_ID + 1
|
|
return this_ID
|
|
end
|
|
|
|
function process.spawn(fn, name, capabilities)
|
|
return spawn(fn, name, nil, capabilities)
|
|
end
|
|
|
|
function process.thread(fn, name)
|
|
local parent = running.name or tostring(running.ID)
|
|
return spawn(fn, ("%s_%s_%04x"):format(name or "th", parent, math.random(0, 0xFFFF)), 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
|
|
|
|
function process.has_grant(g)
|
|
return running.capabilities.grants[g] or running.capabilities.grants[root_capability] or false
|
|
end
|
|
|
|
function process.restriction(r)
|
|
return running.capabilities.restrictions[r] or nil
|
|
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
|
|
|
|
function os.queueEvent(...)
|
|
enqueue(running.ID, {...})
|
|
end
|
|
|
|
local function ancestry_includes(proc, anc)
|
|
repeat
|
|
if proc == anc then
|
|
return true
|
|
end
|
|
proc = proc.parent
|
|
until not proc
|
|
return false
|
|
end
|
|
|
|
function process.is_ancestor(proc, anc)
|
|
return ancestry_includes(processes[proc], processes[anc])
|
|
end
|
|
|
|
function process.queue_in(ID, ...)
|
|
local parent = processes[ID]
|
|
if not parent then error(stringformat("No such process %s.", tostring(ID))) end
|
|
for ID, proc in pairs(processes) do
|
|
if ancestry_includes(proc, parent) and are_capabilities_subset(proc.capabilities, running.capabilities) and not proc.thread then
|
|
enqueue(proc.ID, {...})
|
|
end
|
|
end
|
|
end
|
|
|
|
local dummy_event = ("%07x"):format(math.random(0, 0xFFFFFFF))
|
|
-- Run main event loop
|
|
local function run_loop()
|
|
while true do
|
|
if events_are_queued then
|
|
events_are_queued = false
|
|
for target, events in pairs(queue) do
|
|
for _, event in ipairs(events) do
|
|
tick(processes[target], event)
|
|
end
|
|
queue[target] = nil
|
|
end
|
|
osqueueevent(dummy_event)
|
|
else
|
|
local ev = {coroutineyield()}
|
|
if ev[1] ~= dummy_event then
|
|
for ID, proc in pairs(processes) do
|
|
tick(proc, ev)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function boot()
|
|
if ccemuxecho then ccemuxecho("TLCO executed " .. (debugtraceback and debugtraceback() or "succesfully")) end
|
|
term.redirect(term.native())
|
|
multishell = nil
|
|
term.setTextColor(colors.yellow)
|
|
term.setBackgroundColor(colors.black)
|
|
term.setCursorPos(1,1)
|
|
term.clear()
|
|
|
|
process.spawn(function() os.run({}, "autorun.lua") end, "main", { grants = { [root_capability] = true }, restrictions = {} })
|
|
process.spawn(function()
|
|
-- bodge, because of the rednet bRunning thing
|
|
local old_error = error
|
|
error = function() error = old_error end
|
|
rednet.run()
|
|
end, "rednetd", { grants = {}, restrictions = {} })
|
|
|
|
osqueueevent "" -- tick everything once
|
|
run_loop()
|
|
end
|
|
|
|
-- fixed TLCO from https://gist.github.com/MCJack123/42bc69d3757226c966da752df80437dc
|
|
local old_error = error
|
|
local old_os_shutdown = os.shutdown
|
|
local old_term_redirect = term.redirect
|
|
local old_term_native = term.native
|
|
function error() end
|
|
function term.redirect() end
|
|
function term.native() end
|
|
function os.shutdown()
|
|
error = old_error
|
|
_G.error = old_error
|
|
_ENV.error = old_error
|
|
term.native = old_term_native
|
|
term.redirect = old_term_redirect
|
|
os.shutdown = old_os_shutdown
|
|
os.pullEventRaw = coroutine.yield
|
|
boot()
|
|
end
|
|
|
|
os.pullEventRaw = nil |