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