blob: 15ede5df71b257fe5edd561f983bf48278859b98 [file] [log] [blame] [raw]
local hookInterval = 100
local deadline = math.huge
local hitDeadline = false
local function checkDeadline()
if computer.realTime() > deadline then
debug.sethook(coroutine.running(), checkDeadline, "", 1)
if not hitDeadline then
deadline = deadline + 0.5
end
hitDeadline = true
error("too long without yielding", 0)
end
end
-------------------------------------------------------------------------------
local function checkArg(n, have, ...)
have = type(have)
local function check(want, ...)
if not want then
return false
else
return have == want or check(...)
end
end
if not check(...) then
local msg = string.format("bad argument #%d (%s expected, got %s)",
n, table.concat({...}, " or "), have)
error(msg, 3)
end
end
-------------------------------------------------------------------------------
local function spcall(...)
local result = table.pack(pcall(...))
if not result[1] then
error(tostring(result[2]), 0)
else
return table.unpack(result, 2, result.n)
end
end
local function sgc(self)
local oldDeadline, oldHitDeadline = deadline, hitDeadline
local mt = debug.getmetatable(self)
mt = rawget(mt, "mt")
local gc = rawget(mt, "__gc")
if type(gc) ~= "function" then
return
end
local co = coroutine.create(gc)
debug.sethook(co, checkDeadline, "", hookInterval)
deadline, hitDeadline = math.min(oldDeadline, computer.realTime() + 0.5), true
local result, reason = coroutine.resume(co, self)
debug.sethook(co)
deadline, hitDeadline = oldDeadline, oldHitDeadline
if not result then
error(reason, 0)
end
end
--[[ This is the global environment we make available to userland programs. ]]
-- You'll notice that we do a lot of wrapping of native functions and adding
-- parameter checks in those wrappers. This is to avoid errors from the host
-- side that would push error objects - which are userdata and cannot be
-- persisted.
local sandbox, libprocess
sandbox = {
assert = assert,
dofile = nil, -- in boot/*_base.lua
error = error,
_G = nil, -- see below
getmetatable = function(t)
if type(t) == "string" then -- don't allow messing with the string mt
return nil
end
local result = getmetatable(t)
-- check if we have a wrapped __gc using mt
if type(result) == "table" and rawget(result, "__gc") == sgc then
result = rawget(result, "mt")
end
return result
end,
ipairs = ipairs,
load = function(ld, source, mode, env)
if not system.allowBytecode() then
mode = "t"
end
return load(ld, source, mode, env or sandbox)
end,
loadfile = nil, -- in boot/*_base.lua
next = next,
pairs = pairs,
pcall = function(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end,
print = nil, -- in boot/*_base.lua
rawequal = rawequal,
rawget = rawget,
rawlen = rawlen,
rawset = rawset,
select = select,
setmetatable = function(t, mt)
if type(mt) ~= "table" then
return setmetatable(t, mt)
end
if type(rawget(mt, "__gc")) == "function" then
-- For all user __gc functions we enforce a much tighter deadline.
-- This is because these functions may be called from the main
-- thread under certain circumstanced (such as when saving the world),
-- which can lead to noticeable lag if the __gc function behaves badly.
local sbmt = {} -- sandboxed metatable. only for __gc stuff, so it's
-- kinda ok to have a shallow copy instead... meh.
for k, v in pairs(mt) do
sbmt[k] = v
end
sbmt.mt = mt
sbmt.__gc = sgc
mt = sbmt
end
return setmetatable(t, mt)
end,
tonumber = tonumber,
tostring = tostring,
type = type,
_VERSION = "Lua 5.2",
xpcall = function(f, msgh, ...)
local handled = false
local result = table.pack(xpcall(f, function(...)
if handled then
return ...
else
handled = true
return msgh(...)
end
end, ...))
checkDeadline()
return table.unpack(result, 1, result.n)
end,
coroutine = {
create = coroutine.create,
resume = function(co, ...) -- custom resume part for bubbling sysyields
checkArg(1, co, "thread")
local args = table.pack(...)
while true do -- for consecutive sysyields
debug.sethook(co, checkDeadline, "", hookInterval)
local result = table.pack(
coroutine.resume(co, table.unpack(args, 1, args.n)))
debug.sethook(co) -- avoid gc issues
checkDeadline()
if result[1] then -- success: (true, sysval?, ...?)
if coroutine.status(co) == "dead" then -- return: (true, ...)
return true, table.unpack(result, 2, result.n)
elseif result[2] ~= nil then -- yield: (true, sysval)
args = table.pack(coroutine.yield(result[2]))
else -- yield: (true, nil, ...)
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (false, string)
return false, result[2]
end
end
end,
running = coroutine.running,
status = coroutine.status,
wrap = function(f) -- for bubbling coroutine.resume
local co = coroutine.create(f)
return function(...)
local result = table.pack(sandbox.coroutine.resume(co, ...))
if result[1] then
return table.unpack(result, 2, result.n)
else
error(result[2], 0)
end
end
end,
yield = function(...) -- custom yield part for bubbling sysyields
return coroutine.yield(nil, ...)
end
},
string = {
byte = string.byte,
char = string.char,
dump = string.dump,
find = string.find,
format = string.format,
gmatch = string.gmatch,
gsub = string.gsub,
len = string.len,
lower = string.lower,
match = string.match,
rep = string.rep,
reverse = string.reverse,
sub = string.sub,
upper = string.upper
},
table = {
concat = table.concat,
insert = table.insert,
pack = table.pack,
remove = table.remove,
sort = table.sort,
unpack = table.unpack
},
math = {
abs = math.abs,
acos = math.acos,
asin = math.asin,
atan = math.atan,
atan2 = math.atan2,
ceil = math.ceil,
cos = math.cos,
cosh = math.cosh,
deg = math.deg,
exp = math.exp,
floor = math.floor,
fmod = math.fmod,
frexp = math.frexp,
huge = math.huge,
ldexp = math.ldexp,
log = math.log,
max = math.max,
min = math.min,
modf = math.modf,
pi = math.pi,
pow = math.pow,
rad = math.rad,
random = function(...)
return spcall(math.random, ...)
end,
randomseed = function(seed)
spcall(math.randomseed, seed)
end,
sin = math.sin,
sinh = math.sinh,
sqrt = math.sqrt,
tan = math.tan,
tanh = math.tanh
},
bit32 = {
arshift = bit32.arshift,
band = bit32.band,
bnot = bit32.bnot,
bor = bit32.bor,
btest = bit32.btest,
bxor = bit32.bxor,
extract = bit32.extract,
replace = bit32.replace,
lrotate = bit32.lrotate,
lshift = bit32.lshift,
rrotate = bit32.rrotate,
rshift = bit32.rshift
},
io = nil, -- in lib/io.lua
os = {
clock = os.clock,
date = function(format, time)
return spcall(os.date, format, time)
end,
difftime = function(t2, t1)
return t2 - t1
end,
execute = nil, -- in boot/*_os.lua
exit = nil, -- in boot/*_os.lua
remove = nil, -- in boot/*_os.lua
rename = nil, -- in boot/*_os.lua
time = function(table)
checkArg(1, table, "table", "nil")
return os.time(table)
end,
tmpname = nil, -- in boot/*_os.lua
},
debug = {
traceback = debug.traceback
},
checkArg = checkArg
}
sandbox._G = sandbox
-------------------------------------------------------------------------------
-- Start of non-standard stuff.
-- JNLua derps when the metatable of userdata is changed, so we have to
-- wrap and isolate it, to make sure it can't be touched by user code.
-- These functions provide the logic for wrapping and unwrapping (when
-- pushed to user code and when pushed back to the host, respectively).
local wrapUserdata, wrapSingleUserdata, unwrapUserdata, wrappedUserdataMeta
wrappedUserdataMeta = {
-- Weak keys, clean up once a proxy is no longer referenced anywhere.
__mode="k",
-- We need custom persist logic here to avoid ERIS trying to save the
-- userdata referenced in this table directly. It will be repopulated
-- in the load methods of the persisted userdata wrappers (see below).
[persistKey and persistKey() or "LuaJ"] = function()
return function()
-- When using special persistence we have to manually reassign the
-- metatable of the persisted value.
return setmetatable({}, wrappedUserdataMeta)
end
end
}
local wrappedUserdata = setmetatable({}, wrappedUserdataMeta)
local function processResult(result)
wrapUserdata(result) -- needed for metamethods.
if not result[1] then -- error that should be re-thrown.
error(result[2], 0)
else -- success or already processed error.
return table.unpack(result, 2, result.n)
end
end
local function invoke(target, direct, ...)
local result
if direct then
local args = table.pack(...) -- for unwrapping
unwrapUserdata(args)
result = table.pack(target.invoke(table.unpack(args, 1, args.n)))
if result.n == 0 then -- limit for direct calls reached
result = nil
end
-- no need to wrap here, will be wrapped in processResult
end
if not result then
local args = table.pack(...) -- for access in closure
result = select(1, coroutine.yield(function()
unwrapUserdata(args)
local result = table.pack(target.invoke(table.unpack(args, 1, args.n)))
wrapUserdata(result)
return result
end))
end
return processResult(result)
end
local function udinvoke(f, data, ...)
local args = table.pack(...)
unwrapUserdata(args)
local result = table.pack(f(data, table.unpack(args)))
return processResult(result)
end
-- Metatable for additional functionality on userdata.
local userdataWrapper = {
__index = function(self, ...)
return udinvoke(userdata.apply, wrappedUserdata[self], ...)
end,
__newindex = function(self, ...)
return udinvoke(userdata.unapply, wrappedUserdata[self], ...)
end,
__call = function(self, ...)
return udinvoke(userdata.call, wrappedUserdata[self], ...)
end,
__gc = function(self)
local data = wrappedUserdata[self]
wrappedUserdata[self] = nil
userdata.dispose(data)
end,
-- This is the persistence protocol for userdata. Userdata is considered
-- to be 'owned' by Lua, and is saved to an NBT tag. We also get the name
-- of the actual class when saving, so we can create a new instance via
-- reflection when loading again (and then immediately wrap it again).
-- Collect wrapped callback methods.
[persistKey and persistKey() or "LuaJ"] = function(self)
local className, nbt = userdata.save(wrappedUserdata[self])
-- The returned closure is what actually gets persisted, including the
-- upvalues, that being the classname and a byte array representing the
-- nbt data of the userdata value.
return function()
return wrapSingleUserdata(userdata.load(className, nbt))
end
end,
-- Do not allow changing the metatable to avoid the gc callback being
-- unset, leading to potential resource leakage on the host side.
__metatable = "userdata",
__tostring = function(self)
local data = wrappedUserdata[self]
return tostring(select(2, pcall(data.toString, data)))
end
}
local userdataCallback = {
__call = function(self, ...)
local methods = spcall(userdata.methods, wrappedUserdata[self.proxy])
for name, direct in pairs(methods) do
if name == self.name then
return invoke(userdata, direct, self.proxy, name, ...)
end
end
error("no such method", 1)
end,
__tostring = function(self)
return userdata.doc(wrappedUserdata[self.proxy], self.name) or "function"
end
}
function wrapSingleUserdata(data)
-- Reuse proxies for lower memory consumption and more logical behavior
-- without the need of metamethods like __eq, as well as proper reference
-- behavior after saving and loading again.
for k, v in pairs(wrappedUserdata) do
-- We need a custom 'equals' check for userdata because metamethods on
-- userdata introduced by JNLua tend to crash the game for some reason.
if v == data then
return k
end
end
local proxy = {type = "userdata"}
local methods = spcall(userdata.methods, data)
for method in pairs(methods) do
proxy[method] = setmetatable({name=method, proxy=proxy}, userdataCallback)
end
wrappedUserdata[proxy] = data
return setmetatable(proxy, userdataWrapper)
end
function wrapUserdata(values)
local processed = {}
local function wrapRecursively(value)
if type(value) == "table" then
if not processed[value] then
processed[value] = true
for k, v in pairs(value) do
value[k] = wrapRecursively(v)
end
end
elseif type(value) == "userdata" then
return wrapSingleUserdata(value)
end
return value
end
wrapRecursively(values)
end
function unwrapUserdata(values)
local processed = {}
local function unwrapRecursively(value)
if wrappedUserdata[value] then
return wrappedUserdata[value]
end
if type(value) == "table" then
if not processed[value] then
processed[value] = true
for k, v in pairs(value) do
value[k] = unwrapRecursively(v)
end
end
end
return value
end
unwrapRecursively(values)
end
-------------------------------------------------------------------------------
local libcomponent
local proxyCache = setmetatable({}, {__mode="v"})
local proxyDirectCache = setmetatable({}, {__mode="k"})
local componentCallback = {
__call = function(self, ...)
return invoke(component, not not proxyDirectCache[self], self.address, self.name, ...)
end,
__tostring = function(self)
return libcomponent.doc(self.address, self.name) or "function"
end
}
libcomponent = {
doc = function(address, method)
checkArg(1, address, "string")
checkArg(2, method, "string")
local result, reason = spcall(component.doc, address, method)
if not result and reason then
error(reason, 2)
end
return result
end,
invoke = function(address, method, ...)
checkArg(1, address, "string")
checkArg(2, method, "string")
local methods, reason = spcall(component.methods, address)
if not methods then
return nil, reason
end
for name, direct in pairs(methods) do
if name == method then
return invoke(component, direct, address, method, ...)
end
end
error("no such method", 1)
end,
list = function(filter, exact)
checkArg(1, filter, "string", "nil")
local list = spcall(component.list, filter, not not exact)
local key = nil
return setmetatable(list, {__call=function()
key = next(list, key)
if key then
return key, list[key]
end
end})
end,
methods = function(address)
return spcall(component.methods, address)
end,
proxy = function(address)
local type, reason = spcall(component.type, address)
if not type then
return nil, reason
end
local slot, reason = spcall(component.slot, address)
if not slot then
return nil, reason
end
if proxyCache[address] then
return proxyCache[address]
end
local proxy = {address = address, type = type, slot = slot}
local methods, reason = spcall(component.methods, address)
if not methods then
return nil, reason
end
for method, direct in pairs(methods) do
proxy[method] = setmetatable({address=address,name=method}, componentCallback)
proxyDirectCache[proxy[method]] = direct
end
proxyCache[address] = proxy
return proxy
end,
type = function(address)
return spcall(component.type, address)
end,
slot = function(address)
return spcall(component.slot, address)
end
}
sandbox.component = libcomponent
local libcomputer = {
isRobot = computer.isRobot,
address = computer.address,
tmpAddress = computer.tmpAddress,
freeMemory = computer.freeMemory,
totalMemory = computer.totalMemory,
uptime = computer.uptime,
energy = computer.energy,
maxEnergy = computer.maxEnergy,
getBootAddress = computer.getBootAddress,
setBootAddress = function(...)
return spcall(computer.setBootAddress, ...)
end,
users = computer.users,
addUser = function(...)
return spcall(computer.addUser, ...)
end,
removeUser = function(...)
return spcall(computer.removeUser, ...)
end,
shutdown = function(reboot)
coroutine.yield(reboot ~= nil and reboot ~= false)
end,
pushSignal = function(...)
return spcall(computer.pushSignal, ...)
end,
pullSignal = function(timeout)
local deadline = computer.uptime() +
(type(timeout) == "number" and timeout or math.huge)
repeat
local signal = table.pack(coroutine.yield(deadline - computer.uptime()))
if signal.n > 0 then
return table.unpack(signal, 1, signal.n)
end
until computer.uptime() >= deadline
end,
beep = function(...)
libcomponent.invoke(computer.address(), "beep", ...)
end
}
sandbox.computer = libcomputer
local libunicode = {
char = function(...)
return spcall(unicode.char, ...)
end,
len = function(s)
return spcall(unicode.len, s)
end,
lower = function(s)
return spcall(unicode.lower, s)
end,
reverse = function(s)
return spcall(unicode.reverse, s)
end,
sub = function(s, i, j)
if j then
return spcall(unicode.sub, s, i, j)
end
return spcall(unicode.sub, s, i)
end,
upper = function(s)
return spcall(unicode.upper, s)
end,
isWide = function(s)
return spcall(unicode.isWide, s)
end,
charWidth = function(s)
return spcall(unicode.charWidth, s)
end,
wlen = function(s)
return spcall(unicode.wlen, s)
end,
wtrunc = function(s, n)
return spcall(unicode.wtrunc, s, n)
end
}
sandbox.unicode = libunicode
-------------------------------------------------------------------------------
local function bootstrap()
function boot_invoke(address, method, ...)
local result = table.pack(pcall(invoke, component, true, address, method, ...))
if not result[1] then
return nil, result[2]
else
return table.unpack(result, 2, result.n)
end
end
do
local screen = libcomponent.list("screen")()
local gpu = libcomponent.list("gpu")()
if gpu and screen then
boot_invoke(gpu, "bind", screen)
end
end
local function tryLoadFrom(address)
local handle, reason = boot_invoke(address, "open", "/init.lua")
if not handle then
return nil, reason
end
local buffer = ""
repeat
local data, reason = boot_invoke(address, "read", handle, math.huge)
if not data and reason then
return nil, reason
end
buffer = buffer .. (data or "")
until not data
boot_invoke(address, "close", handle)
return load(buffer, "=init", "t", sandbox)
end
local init, reason
if computer.getBootAddress() then
init, reason = tryLoadFrom(computer.getBootAddress())
end
if not init then
computer.setBootAddress()
for address in libcomponent.list("filesystem") do
init, reason = tryLoadFrom(address)
if init then
computer.setBootAddress(address)
break
end
end
end
if not init then
error("no bootable medium found" .. (reason and (": " .. tostring(reason)) or ""), 0)
end
return coroutine.create(init), {n=0}
end
-------------------------------------------------------------------------------
local function main()
-- Yield once to get a memory baseline.
coroutine.yield()
-- After memory footprint to avoid init.lua bumping the baseline.
local co, args = bootstrap()
local forceGC = 10
while true do
deadline = computer.realTime() + system.timeout()
hitDeadline = false
-- NOTE: since this is run in an executor thread and we enforce timeouts
-- in user-defined garbage collector callbacks this should be safe.
if persistKey then -- otherwise we're in LuaJ
forceGC = forceGC - 1
if forceGC < 1 then
collectgarbage("collect")
forceGC = 10
end
end
debug.sethook(co, checkDeadline, "", hookInterval)
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
if not result[1] then
error(tostring(result[2]), 0)
elseif coroutine.status(co) == "dead" then
error("computer stopped unexpectedly", 0)
else
args = table.pack(coroutine.yield(result[2])) -- system yielded value
wrapUserdata(args)
end
end
end
-- JNLua converts the coroutine to a string immediately, so we can't get the
-- traceback later. Because of that we have to do the error handling here.
return pcall(main)