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
for addr, kind in pairs(component.list()) do
if kind == "eeprom" then
local data = component.invoke(addr, "get")
assert(load(data, "=eeprom"))()
return
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
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 *_) {