basic sandbox progress

This commit is contained in:
IonutParau 2025-05-26 18:07:20 +02:00
parent 5dd6f77cc4
commit 5d084a51b9
2 changed files with 451 additions and 24 deletions

View File

@ -1,35 +1,427 @@
-- sandbox stuff
local clist = component.list
function component.list(type, exact)
local m = clist()
if not type then return m end
local function copy(v, p)
if type(v) == "table" then
local t = {}
for addr, kind in pairs(m) do
if exact then
if type == kind then
t[addr] = kind
for key, val in pairs(v) do
t[key] = copy(val)
end
if p then
for key, val in pairs(p) do
if not rawget(t, key) then
t[key] = copy(val)
end
end
end
return t
else
return v
end
end
local function nextDeadline()
return computer.uptime() + 5
end
local bubbleYield = false
local timeout = nextDeadline()
local function tooLong()
return computer.uptime() >= timeout
end
local tooLongWithoutYielding = "too long without yielding"
local function nextHeatUp()
return computer.uptime() + math.random() * 2 + 0.1
end
local heatInc = nextHeatUp()
debug.sethook(function()
if computer.uptime() >= heatInc then
heatInc = nextHeatUp()
computer.addHeat(math.random() * 3)
end
if tooLong() and not bubbleYield then
bubbleYield = true
error(tooLongWithoutYielding) -- here it is an actual string
end
end, "c", 20000) -- no bogo mips, the check is cheap anyways
local function resume(co, val1, ...)
while true do
local t = {coroutine.resume(co, val1, ...)}
if bubbleYield then -- yield was meaningless
coroutine.yield() -- carry through
else
return table.unpack(t) -- yield the user cares about
end
end
end
local function yield()
bubbleYield = true
coroutine.yield()
end
local function ensureYields()
if bubbleYield then
coroutine.yield()
end
end
local function checkArg(idx, v, ...)
local bad = true
local n = select("#", ...)
for i=1,n do
local t = select(i, ...)
if type(v) == t then bad = false break end
end
if not bad then return end
local msg = string.format("bad argument #%d (%s expected, got %s)", idx, table.concat({...}, " or "), type(v))
error(msg, 3)
end
local libcomponent
local componentProxy = {
__pairs = function(self)
local method
return function()
method = next(self, method)
if method then
return method, self[method]
end
end
end,
}
local componentCallback = {
__call = function(self, ...)
return libcomponent.invoke(self.address, self.name, ...)
end,
__tostring = function(self)
return libcomponent.doc(self.address, self.name) or "undocumented"
end
}
libcomponent = {
invoke = function(addr, method, ...)
checkArg(1, addr, "string")
checkArg(2, method, "string")
while true do
local r = {pcall(component.invoke, addr, method, ...)}
-- in this situation, either the temperature is above 100 C and we throttle
-- or the call budget has been filled and we dont care
if computer.isOverheating() or computer.isOverworked() then
local ok = pcall(yield)
assert(ok, "component explicitly requested to be suspended")
end
if computer.getState() == states.blackout then
-- oops, powerout
local ok = pcall(yield)
assert(ok, "blackout")
end
if computer.getState() ~= states.busy then
-- busy gets to try again
if r[1] then
return table.unpack(r, 2)
end
return nil, r[2]
end
end
end,
list = function(filter, exact)
checkArg(1, filter, "string", "nil")
local t = component.list()
if not filter then return t end
local list = {}
for addr, kind in pairs(t) do
if type(exact) == "boolean" and exact then
if kind == filter then
list[addr] = kind
end
elseif rawequal(exact, "pattern") then
if string.match(kind, filter) then
list[addr] = kind
end
else
if string.match(kind, type) then
t[addr] = kind
if string.find(kind, filter, nil, true) then
list[addr] = kind
end
end
end
setmetatable(t, {
local key = nil
return setmetatable(list, {
__call = function()
return next(t)
key = next(list, key)
if key then
return key, list[key]
end
end,
})
return t
end,
methods = component.methods,
fields = component.fields,
doc = component.doc,
slot = component.slot,
type = component.type,
proxy = function(addr)
checkArg(1, addr, "string")
if not component.type(addr) then return nil, "no such component" end
local proxy = setmetatable({
address = addr,
type = component.type(addr),
slot = component.slot(addr),
fields = {},
}, componentProxy)
local methods = component.methods(addr)
for method in pairs(methods) do
proxy[method] = setmetatable({address = addr, name = method}, componentCallback)
end
return proxy
end,
}
local libcomputer = {
isRobot = function()
return libcomponent.list("robot", true) ~= nil
end,
address = computer.address,
tmpAddress = computer.tmpAddress,
usedMemory = computer.usedMemory,
freeMemory = computer.freeMemory,
totalMemory = computer.totalMemory,
uptime = computer.uptime,
energy = computer.energy,
maxEnergy = computer.maxEnergy,
users = computer.users,
-- these 2 are not actually implemented
-- TODO: implement them
addUser = computer.addUser,
removeUser = computer.removeUser,
shutdown = function(reboot)
computer.setState(reboot and states.REPEAT or states.closing)
yield()
end,
pushSignal = computer.pushSignal,
pullSignal = function(timeout)
local deadline = computer.uptime() + (type(timeout) == "number" and timeout or math.huge)
repeat
yield() -- give executor a chance to give us stuff
local s = table.pack(computer.pullSignal())
if s.n > 0 then
return table.unpack(s)
end
until computer.uptime() >= deadline
end,
beep = computer.beep,
getDeviceInfo = function()
return {} -- yup
end,
getProgramLocations = function()
return {} -- yup
end,
getArchitectures = computer.getArchitectures,
getArchitecture = computer.getArchitecture,
setArchitecture = function(...)
computer.setArchitecture(...) -- also sets state to SWITCH
yield()
end,
getTemperature = computer.getTemperature,
}
local sandbox
sandbox = {
assert = assert,
error = error,
getmetatable = function(t)
if type(t) == "string" then -- HUGE security problem
return nil -- fixed
end
return getmetatable(t)
end,
ipairs = ipairs,
load = function(ld, source, _, env) -- mode is ignored as bytecode is just fully illegal for now
return load(ld, source, "t", env or sandbox)
end,
next = next,
pairs = pairs,
pcall = function(...)
if tooLong() then
yield()
return false, tooLongWithoutYielding
end
local t = {pcall(...)}
ensureYields() -- if it took too long, this will make it yield
return table.unpack(t)
end,
rawequal = rawequal,
rawget = rawget,
rawlen = rawlen,
rawset = rawset,
select = select,
setmetatable = function(t, mt)
if type(mt) ~= "table" then
return setmetatable(t, mt)
end
-- we do mutate the metatable but this field shouldn't exist anyways
mt.__gc = nil
return setmetatable(t, mt)
end,
tonumber = tonumber,
tostring = tostring,
type = type,
_VERSION = _VERSION,
xpcall = function(f, msgh, ...)
checkArg(1, f, "function")
checkArg(2, msgh, "function")
-- to prevent infinite loops we simply terminate the error handler if it took too long.
local function errorCapture(ff, ...)
--ensureYields() -- you can't yield in xpcall...
-- Immediately dont care
if tooLong() then
return nil, tooLongWithoutYielding
end
-- This would mean you shutdown in the errorCapture.
-- In vanilla OC, that does nothing.
-- In here, it returns a suspended error and then yields eventually.
if bubbleYield then
return nil, "suspended"
end
return xpcall(ff, function(...)
if tooLong() then
return tooLongWithoutYielding
else
return select(2, errorCapture(msgh, ...))
end
end, ...)
end
local t = {errorCapture(f, ...)}
pcall(ensureYields) -- it can fail if we are doing xpcall in xpcall.
return table.unpack(t)
end,
coroutine = {
create = coroutine.create,
resume = resume,
running = coroutine.running,
status = coroutine.status,
yield = coroutine.yield,
wrap = function(f)
-- uses the correct resume
local co = coroutine.create(f)
return function(...)
local result = {resume(co, ...)}
if result[1] then
return table.unpack(result, 2)
else
error(result[2], 0)
end
end
end,
isyieldable = coroutine.isyieldable,
},
string = copy(string),
table = copy(table),
math = copy(math, {
-- patch table
atan2 = math.atan,
ldexp = function(a, e) return a * (2.0 ^ e) end,
pow = function(a, b) return a ^ b end,
}),
os = {
clock = os.clock,
date = os.date,
difftime = function(t2, t1) return t2 - t1 end, -- thanks UNIX
time = function(t)
checkArg(1, t, "table", "nil")
return os.time(t)
end,
},
debug = {
getinfo = function(...)
local result = debug.getinfo(...)
if result then
return {
source = result.source,
short_src = result.short_src,
linedefined = result.linedefined,
lastlinedefined = result.lastlinedefined,
what = result.what,
currentline = result.currentline,
nups = result.nups,
nparams = result.nparams,
isvararg = result.isvararg,
name = result.name,
namewhat = result.namewhat,
istailcall = result.istailcall,
-- believe it or not, this IS NOT safe.
-- They may use this to re-call machine.lua which would reset the hook and timeout.
-- TODO: make this safe.
--func = result.func,
}
end
end,
traceback = debug.traceback,
-- we only allow the first return, aka not the value.
-- Otherwise, some dumb shmuck could do nasty stuff.
-- TODO: make them not need this.
getlocal = function(...) return (debug.getlocal(...)) end,
getupvalue = function(...) return (debug.getupvalue(...)) end,
},
utf8 = copy(utf8),
checkArg = checkArg,
component = libcomponent,
computer = libcomputer,
}
sandbox._G = sandbox
local function bootstrap()
local eeprom = libcomponent.list("eeprom")()
assert(eeprom, "no eeprom")
local code = libcomponent.invoke(eeprom, "get")
assert(code and #code > 0, "empty eeprom")
return assert(load(code, "=bios", "t", sandbox))
end
for addr, kind in pairs(component.list()) do
if kind == "eeprom" then
local data = component.invoke(addr, "get")
assert(load(data, "=eeprom"))()
return
coroutine.yield() -- startup delay
local f = bootstrap()
local co = coroutine.create(f)
while true do
timeout = nextDeadline()
bubbleYield = false
local ok, err = resume(co)
if not ok then
error(err)
elseif coroutine.status(co) == "dead" then
error("computer halted", 0)
else
coroutine.yield()
end
end
error("no bios")
error("machine halted")

View File

@ -272,6 +272,19 @@ static int testLuaArch_computer_users(lua_State *L) {
return i;
}
static int testLuaArch_computer_getState(lua_State *L) {
nn_computer *c = testLuaArch_getComputer(L);
lua_pushinteger(L, nn_getState(c));
return 1;
}
static int testLuaArch_computer_setState(lua_State *L) {
nn_computer *c = testLuaArch_getComputer(L);
int s = luaL_checkinteger(L, 1);
nn_setState(c, s);
return 1;
}
static int testLuaArch_component_list(lua_State *L) {
nn_computer *c = testLuaArch_getComputer(L);
size_t len = 0;
@ -446,6 +459,10 @@ void testLuaArch_loadEnv(lua_State *L) {
lua_setfield(L, computer, "popSignal");
lua_pushcfunction(L, testLuaArch_computer_users);
lua_setfield(L, computer, "users");
lua_pushcfunction(L, testLuaArch_computer_getState);
lua_setfield(L, computer, "getState");
lua_pushcfunction(L, testLuaArch_computer_setState);
lua_setfield(L, computer, "setState");
lua_setglobal(L, "computer");
lua_createtable(L, 0, 10);
@ -465,6 +482,24 @@ void testLuaArch_loadEnv(lua_State *L) {
lua_pushcfunction(L, testLuaArch_component_type);
lua_setfield(L, component, "type");
lua_setglobal(L, "component");
lua_createtable(L, 0, 7);
int states = lua_gettop(L);
lua_pushinteger(L, NN_STATE_SETUP);
lua_setfield(L, states, "setup");
lua_pushinteger(L, NN_STATE_RUNNING);
lua_setfield(L, states, "running");
lua_pushinteger(L, NN_STATE_BUSY);
lua_setfield(L, states, "busy");
lua_pushinteger(L, NN_STATE_BLACKOUT);
lua_setfield(L, states, "blackout");
lua_pushinteger(L, NN_STATE_CLOSING);
lua_setfield(L, states, "closing");
lua_pushinteger(L, NN_STATE_REPEAT);
lua_setfield(L, states, "REPEAT");
lua_pushinteger(L, NN_STATE_SWITCH);
lua_setfield(L, states, "switch");
lua_setglobal(L, "states");
}
testLuaArch *testLuaArch_setup(nn_computer *computer, void *_) {