resumable computers; signals appear to work; fixed bug in allocator in jnlua

This commit is contained in:
Florian Nücke 2013-08-31 17:58:17 +02:00
parent 4e820da1f2
commit 0327cec937
16 changed files with 1117 additions and 1002 deletions

View File

@ -0,0 +1,210 @@
--[[ Low level boot logic. ]]
--[[ Argument checking for functions. ]]
function checkType(n, have, ...)
have = type(have)
for _, want in pairs({...}) do
if have == want then return end
end
error("bad argument #" .. n .. " (" .. table.concat({...}, " or ") ..
" expected, got " .. have .. ")", 3)
end
--[[ The following code is used to allow making tables read-only. It is, by and
large, inspired by http://lua-users.org/wiki/RecursiveReadOnlyTables
We override some library functions in the sandbox we create to enforce
honoring the fact that tables are readonly, such as rawset.
--]]
-- Store metatables that have been made read-only. This allows us to uniquely
-- identify such tables, without users being able to fake it.
local roproxies = setmetatable({}, {__mode="kv"}) -- real table -> proxy
local rometatables = setmetatable({}, {__mode="k"}) -- proxy -> metatable
--[[ Create a read-only proxy of a table. ]]
function table.asreadonly(t)
checkType(1, t, "table")
local wrap
local function roindex(t)
return function(_, k)
local value = t[k]
if type(value) == "table" then
value = wrap(value) -- wrap is cached
end
return value
end
end
local function ronewindex(_, _, _)
error("trying to modify read-only table", 2)
end
local function ropairs(t)
local function ronext(_, k)
local nk, nv = next(t, k)
if type(nv) == "table" then
nv = wrap(nv) -- wrap is cached
end
return nk, nv
end
return function(_)
return ronext, nil, nil
end
end
local function wrap(t)
if not roproxies[t] then
local metatable = { __index = roindex(t),
__newindex = ronewindex,
__pairs = ropairs(t),
__metatable = "read only table" }
rometatables[proxy] = metatable
roproxies[t] = setmetatable({}, metatable)
end
return roproxies[t]
end
return wrap(t)
end
--[[ Allow checking if a table is read-only. ]]
function table.isreadonly(t)
return rometatables[t] ~= nil
end
--[[ Because I need this this every so often I decided to include it in the
base API. This allows copying tables shallow or deep, to a new table or
into an existing one. Usage:
table.copy(t) -- new table, shallow copy
table.copy(t, true) -- new table, deep copy
table.copy(t1, t2) -- copy t1 to t2, shallow copy
table.copy(t1, t2, true) -- copy t1 to t2, deep copy
--]]
function table.copy(from, to, deep)
checkType(1, from, "table")
checkType(2, to, "table", "boolean", "nil")
checkType(3, deep, "boolean", "nil")
deep = deep or (to and type(to) ~= "table")
local copied, shallowcopy, deepcopy = {}
function shallowcopy(from, to)
for k, v in pairs(from) do
to[k] = (deep and type(v) == "table") and deepcopy(v) or v
end
return to
end
function deepcopy(t)
if copied[t] then return copied[t] end
copied[t] = {}
return shallowcopy(t, copied[t])
end
return shallowcopy(from, type(to) == "table" and to or {})
end
--[[ Wrap all driver callbacks.
For each driver we generate a wrapper that will yield a closure that
will perform the actual call. This way the actual call can be performed
in the server thread, meaning we don't have to worry about mutlithreading
interaction with other components of Minecraft.
--]]
do
-- OK, I admit this is a little crazy... here goes:
local function wrap(f)
-- This is the function that replaces the original API function. It is
-- called from userland when it wants something from a driver.
return function(...)
local args = {...}
-- What it does, is that it yields a function. That function is called
-- from the server thread, to ensure synchronicity with the world.
local result = coroutine.yield(function()
-- It runs the actual API function protected mode. We return this as
-- a table because a) we need it like this on the outside anyway and
-- b) only the first item in the global stack is persisted.
return {pcall(f, table.unpack(args))}
end)
-- The next time our executor runs it pushes that result and calls
-- resume, so we get it via the yield. Thus: result = pcall(f, ...)
if result[1] then
-- API call was successful, return the results.
return select(2, table.unpack(result))
else
-- API call failed, re-throw the error. We apply tostring to it
-- because JNLua pushes the original Java exceptions.
error(tostring(result[2]), 2)
end
end
end
-- There really shouldn't be any cycles in the API table, but to be safe...
local done = {}
local function wrapRecursive(t)
if done[t] then return end
done[t] = true
for k, v in pairs(t) do
if type(v) == "function" then
t[k] = wrap(v)
elseif type(v) == "table" then
wrapRecursive(v)
end
end
end
wrapRecursive(drivers)
end
--[[ Permanent value tables.
These tables must contain all java callbacks (i.e. C functions, since
they are wrapped on the native side using a C function, of course).
They are used when persisting/unpersisting the state so that the
persistence library knows which values it doesn't have to serialize
(since it cannot persist C functions).
These tables may change after loading a game, for example due to a new
mod being installed or an old one being removed. In that case, the
persistence library will throw an error while unpersisting, leading
to what will essentially be a computer crash; which is pretty much
the best way to tackle this, I think.
--]]
do
local perms, uperms = {}, {}
--[[ Used by the Java side to persist the state when the world is saved. ]]
function persist(kernel)
return eris.persist(perms, kernel)
end
--[[ Used by the Java side unpersist the state when the world is loaded. ]]
function unpersist(value)
if value and type(value) == "string" and value:len() > 0 then
return eris.unpersist(uperms, value)
else
return nil
end
end
--[[ Flattens nested tables to concatenate field names with points. This is
done to ensure we don't have any duplicates and to get the perm "names".
--]]
local function flattenAndStore(k, v)
-- We only care for tables and functions, any value types are safe.
if type(v) == "table" or type(v) == "function" then
assert(uperms[k] == nil, "duplicate permanent value named " .. k)
-- If we have aliases its enough to store the value once.
if perms[v] then return end
perms[v] = k
uperms[k] = v
-- Recurse into tables.
if type(v) == "table" then
-- Enforce a deterministic order when determining the keys, to ensure
-- the keys are the same when unpersisting again.
local keys = {}
for ck, _ in pairs(v) do
table.insert(keys, ck)
end
table.sort(keys)
for _, ck in ipairs(keys) do
flattenAndStore(k .. "." .. ck, v[ck])
end
end
end
end
-- Mark everything that's globally reachable at this point as permanent.
flattenAndStore("_ENV", _ENV)
end

View File

@ -1,155 +1,339 @@
--[[
Basic OS functionality, such as launching new programs and loading drivers.
--[[ Basic functionality, drives userland and enforces timeouts.
This is called as the main coroutine by the computer. If this throws, the
computer crashes. If this returns, the computer is considered powered off.
]]
--]]
--[[ Will be adjusted by the kernel when running, represents how long we can
continue running without yielding. Used in the debug hook that enforces
this timeout by throwing an error if it's exceeded. ]]
local deadline = 0
--[[ The hook installed for process coroutines enforcing the timeout. ]]
local function timeoutHook()
local now = os.clock()
if now > deadline then
error({timeout=debug.traceback(2)}, 0)
end
end
--[[ Set up the global environment we make available to userland programs. ]]
local function buildSandbox()
local sandbox = {
-- Top level values. The selection of kept methods rougly follows the list
-- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes
assert = assert,
error = error,
pcall = pcall,
xpcall = xpcall,
ipairs = ipairs,
next = next,
pairs = pairs,
rawequal = rawequal,
rawget = rawget,
rawlen = rawlen,
rawset = rawset,
select = select,
type = type,
tonumber = tonumber,
tostring = tostring,
-- We don't care what users do with metatables. The only raised concern was
-- about breaking an environment, and we don't care about that.
getmetatable = getmetatable,
setmetatable = setmetatable,
-- Custom print that actually writes to the screen buffer.
print = print,
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
},
coroutine = {
create = coroutine.create,
resume = coroutine.resume,
running = coroutine.running,
status = coroutine.status,
wrap = coroutine.wrap,
yield = coroutine.yield
},
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 = math.random,
randomseed = math.randomseed,
sin = math.sin,
sinh = math.sinh,
sqrt = math.sqrt,
tan = math.tan,
tanh = math.tanh
},
os = {
clock = os.clock,
date = os.date,
difftime = os.difftime,
time = os.time,
freeMemory = os.freeMemory,
totalMemory = os.totalMemory
},
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 = unpack,
-- Custom functions.
copy = table.copy,
asreadonly = table.asreadonly,
isreadonly = table.isreadonly
}
}
sandbox._G = sandbox
-- Allow sandboxes to load code, but only in text form, and in the sandbox.
-- Note that we allow passing a custom environment, because if this is called
-- from inside the sandbox, env must already be in the sandbox.
function sandbox.load(code, env)
return load(code, nil, "t", env or sandbox)
end
-- Make methods respect the read-only aspect of tables.
do
local function checkreadonly(t)
if table.isreadonly(t) then
error("trying to modify read-only table", 3)
end
end
function sandbox.rawset(t, k, v)
checkreadonly(t)
rawset(t, k, v)
end
function sandbox.table.insert(t, k, v)
checkreadonly(t)
table.insert(t, k, v)
end
function sandbox.table.remove(t, k)
checkreadonly(t)
table.remove(t, k)
end
function sandbox.table.sort(t, f)
checkreadonly(t)
table.sort(t, f)
end
end
--[[ Error thrower available to the userland. Used to differentiate system
errors from user errors, such as timeouts (we rethrow system errors).
--]]
function sandbox.error(message, level)
level = math.max(0, level or 1)
error({message=message}, level > 0 and level + 1 or 0)
end
local function checkResult(success, result, ...)
if success then
return success, result, ...
end
if result.timeout then
error({timeout=result.timeout .. "\n" .. debug.traceback(2)}, 0)
end
return success, result.message
end
function sandbox.pcall(f, ...)
return checkResult(pcall(f, ...))
end
function sandbox.xpcall(f, msgh, ...)
function handler(msg)
return msg.message and {message=msgh(msg.message)} or msg
end
return checkResult(xpcall(f, handler, ...))
end
--[[ Install wrappers for coroutine management that reserves the first value
returned by yields for internal stuff.
--]]
function sandbox.coroutine.yield(...)
return coroutine.yield(nil, ...)
end
function sandbox.coroutine.resume(co, ...)
if not debug.gethook(co) then -- Don't reset counter.
debug.sethook(co, timeoutHook, "", 10000)
end
local result = {checkResult(coroutine.resume(co, ...))}
if result[1] and result[2] then
-- Internal yield, bubble up to the top and resume where we left off.
return coroutine.resume(co, coroutine.yield(result[2]))
end
-- Userland yield or error, just pass it on.
return result[1], select(3, table.unpack(result))
end
--[[ Suspends the computer for the specified amount of time. Note that
signal handlers will still be called if a signal arrives.
--]]
function sandbox.os.sleep(seconds)
checkType(1, seconds, "number")
local target = os.clock() + seconds
while os.clock() < target do
-- Yielding a number here will tell the host it can wait with running us
-- again for that long. Note that this is *not* a sleep! We may be resumed
-- way sooner, e.g. because of signals or a state load (after an unload).
-- That's why we put a loop around the thing.
coroutine.yield(seconds)
end
end
return sandbox
end
local function main()
--[[
A copy the globals table to avoid user-space programs messing with us. The
actual copy is created below, because we first need to declare the table copy
function... which in turn uses this variable to avoid being tampered with.
]]
local g = _G
--[[ Create the sandbox as a thread-local variable so it is persisted. ]]
local sandbox = buildSandbox()
-- List of all active processes.
local processes = {}
-- The ID of the process currently running.
local currentProcess = 0
--[[
Returns the process ID of the currently executing process.
]]
function _G.os.pid()
return currentProcess
end
-- Starts a new process using the specified callback.
function _G.os.execute(task)
local callback = task
if g.type(task) == "string" then
-- Check if we have a file system, load script and set callback.
-- TODO ...
g.setfenv(callback, g.setmetatable({}, {__index = _G}))
end
g.table.insert(processes, {
pid = #processes,
thread = g.coroutine.create(callback),
parent = currentProcess,
sleep = 0,
signals = {}
})
end
--[[ Stops the process currently being executed. ]]
function _G.os.exit()
g.coroutine.yield("terminate")
end
--[[ Makes the current process sleep for the specified amount of time. ]]
function _G.os.sleep(seconds)
assert(g.type(seconds) == "number",
g.string.format("'number' expected, got '%s'", g.type(seconds)))
processes[currentProcess].sleep = g.os.clock() + seconds
while processes[currentProcess].sleep > g.os.clock() do
local signal = {g.coroutine.yield()}
if signal[1] then
processes[currentProcess][signal[1]](g.unpack(signal))
end
end
end
--[[ List of signal handlers, by name. ]]
local signals = setmetatable({}, {__mode = "v"})
--[[ Registers or unregisters a callback for a signal. ]]
function _G.os.signal(name, callback)
assert(g.type(name) == "string" and g.type(callback) == "function",
g.string.format("'string', 'function' expected, got '%s', '%s'",
g.type(name), g.type(callback)))
processes[currentProcess][name] = callback
function sandbox.os.signal(name, callback)
checkType(1, name, "string")
checkType(2, callback, "function", "nil")
local oldCallback = signals[name]
signals[name] = callback
return oldCallback
end
-- We replace the default yield function so that be can differentiate between
-- process level yields and coroutine level yields - in case a process spawns
-- new coroutines.
--[[
function _G.coroutine.yield(...)
while true do
local result = {g.coroutine.yield(nil, ...)}
if result[1] == "signal" then
end
end
end
function _G.coroutine.resume(...)
while true do
local result = {g.coroutine.resume(...)}
if result[1] and result[2] == "signal" then
--[[ Error handler for signal callbacks. ]]
local function onSignalError(msg)
if type(msg) == "table" then
if msg.timeout then
msg = "too long without yielding"
else
return result[1], g.select(3, g.unpack(result))
msg = msg.message
end
end
print(msg)
return msg
end
]]
-- Create the actual copy now that we have our copying function.
g = g.table.copy(g, true)
--[[ Set up the shell, which is really just a Lua interpreter. ]]
local shellThread
local function startShell(safeMode)
-- Set sandbox environment and create the shell runner.
local _ENV = sandbox
local function shell()
function test(arg)
print(string.format("%d SIGNAL! Available RAM: %d/%d",
os.time(), os.freeMemory(), os.totalMemory()))
end
os.signal("test", test)
-- Spawn the init process, which is basically a Lua interpreter.
g.os.execute(function() print("hi") os.sleep(5) end)
local i = 0
while true do
i = i + 1
print("ping " .. i)
os.sleep(1)
end
end
shellThread = coroutine.create(shell)
end
startShell()
print("Running kernel...")
while true do
print("ping")
g.coroutine.yield(5)
end
-- Begin running our processes. We run all processes consecutively if they are
-- currently "awake", meaning not waiting for a call to os.sleep to return. If
-- a signal arrives and the process has a callback for it, it is still resumed,
-- though, to call the callback.
-- If all processes are asleep, we yield the minimum sleep time, so that we can
-- avoid busy waiting (resuming the main coroutine over and over again).
local sleep = 0
while #processes > 0 do
local signal = {g.coroutine.yield(sleep)}
local signalPid = signal[1]
local signalName = signal[2]
local signalArgs = g.select(3, g.unpack(signal))
for _, process in ipairs(processes) do
local awake = g.os.clock() >= process.sleep
local target = process.signals[signalName] and
(signalPid < 1 or signalPid == process.pid)
if awake or target then
currentProcess = process.pid
local result, cause = g.coroutine.resume(process.thread, "signal", signalName, g.unpack(signalArgs))
if not result or g.coroutine.status(process.thread) == "dead" then
process.thread = nil
elseif cause == "terminate" then
process.thread = nil
end
end
end
sleep = g.math.huge
for i = #processes, 1, -1 do
if processes[i].thread == nil then
g.table.remove(processes, i)
-- Pending signal to be processed. We only either process a signal *or* run
-- the shell, to keep the chance of long blocks low (and "accidentally" going
-- over the timeout).
local signal
while coroutine.status(shellThread) ~= "dead" do
deadline = os.clock() + 5
local result
if signal and signals[signal[1]] then
xpcall(signals[signal[1]], onSignalError, select(2, table.unpack(signal)))
else
sleep = g.math.min(processes[i].sleep, sleep)
if not debug.gethook(shellThread) then
debug.sethook(shellThread, timeoutHook, "", 10000)
end
local status = {coroutine.resume(shellThread)}
if status[1] then
-- All is well, in case we had a yield return the yielded value.
if coroutine.status(shellThread) ~= "dead" then
result = status[2]
end
elseif status[2].timeout then
-- Timeout, restart the shell but don't start user scripts this time.
startShell(true)
else
-- Some other error, go kill ourselves.
error(result[2], 0)
end
end
sleep = g.math.max(0, sleep - g.os.clock())
signal = {coroutine.yield(result)}
end
end
-- local result, message = pcall1(function()
-- JNLua (or possibly Lua itself) sucks at propagating error messages across
-- resumes that were triggered from the native side, so we do a pcall here.
local result, message = pcall(main)
-- if not result then error(message) end end)
if not result then
print(message)
end
--if not result then
print(result, message)
--end
return result, message

View File

@ -1,172 +0,0 @@
--[[
This script sets up the environment to properly allow persisting the state
after this point, regardless of what the following code does.
]]
-- Keep a backup of globals that we use for direct access.
local g = {
assert = assert,
error = error,
next = next,
pcall = pcall,
type = type,
unpack = unpack,
coroutine = {
create = coroutine.create,
resume = coroutine.resume,
status = coroutine.status,
yield = coroutine.yield
},
debug = {
traceback = debug.traceback
}
}
_G.debug = nil
--[[
Replace functions known to call back to Lua with ones writtin in Lua. This
is necessary for yielding to work in Lua 5.1, where it's not possible to
yield across C stack frames. Note that even though this works in 5.2, it's
probably nigh impossible to serialize such yielded coroutines, exactly
because of the C stack frames.
]]
-- Wrap all native functions with a wrapper that generates a traceback.
local function wrapper(f, name)
return function(...)
local result = {g.pcall(f, ...)}
if result[1] then
return g.unpack(result, 2)
else
g.error(g.debug.traceback(result[2], 2), 2)
end
end
end
local function wrap(t)
local walked = {}
local towrap = {}
local function walk(t)
for k, v in pairs(t) do
if not walked[v] then
walked[v] = true
if type(v) == "function" then
table.insert(towrap, {t, k, v})
elseif type(v) == "table" then
walk(v)
end
end
end
end
walk(t)
for _, v in ipairs(towrap) do
local t, k, v = unpack(v)
t[k] = wrapper(v, k)
end
end
wrap(_G)
function _G.xpcall(...)
local args = {...}
g.assert(#args > 1, "bad argument #2 to 'xpcall' (value expected)")
local f = args[1]
local msgh = args[2]
local result
if g.type(f) == "function" then
local co = g.coroutine.create(f)
result = {g.coroutine.resume(co, unpack(args, 3))}
while g.coroutine.status(co) ~= "dead" do
result = {g.coroutine.resume(co, g.coroutine.yield(g.unpack(result, 2)))}
end
else
result = {false, "attempt to call a " .. g.type(f) .. " value"}
end
if result[1] then
return g.unpack(result)
end
if g.type(msgh) == "function" then
local ok, message = g.pcall(msgh, g.unpack(result, 2))
if ok then
return false, message
end
end
return false, "error in error handling"
end
g.xpcall = xpcall
local function passthrough(msg) return msg end
function _G.pcall(f, ...)
return g.xpcall(f, passthrough, ...)
end
function _G.ipairs(...)
local args = {...}
g.assert (#args > 0, "bad argument #1 to 'ipairs' (table expected, got no value)")
local t = args[1]
g.assert(g.type(t) == "table", "bad argument #1 to 'ipairs' (table expected, got" .. g.type(t) .. ")")
return function(t, idx)
idx = idx + 1
local value = t[idx]
if value then
return idx, value
end
end, t, 0
end
function _G.pairs(...)
local args = {...}
g.assert (#args > 0, "bad argument #1 to 'pairs' (table expected, got no value)")
local t = args[1]
g.assert(g.type(t) == "table", "bad argument #1 to 'pairs' (table expected, got" .. g.type(t) .. ")")
return g.next, t, nil
end
function _G.coroutine.wrap(...)
local args = {...}
g.assert (#args > 0, "bad argument #1 to 'wrap' (function expected, got no value)")
local f = args[1]
g.assert(g.type(f) == "function", "bad argument #1 to 'wrap' (function expected, got" .. g.type(f) .. ")")
local co = g.coroutine.create(f)
return function(...)
local result = {g.coroutine.resume(co, ...)}
if result[1] then
return g.unpack(result, 2)
else
g.error(result[2], 2)
end
end
end
--[[ Build Pluto's permanent value tables. ]]
local perms, uperms = {[_ENV] = "_ENV"}, {["_ENV"] = _ENV}
-- Flattens nested tables to concatenate field names with points. This is done
-- to ensure we don't have any duplicates and to get the perm "names".
local function store(t)
if not t then return end
local done = {}
local function flattenAndStore(k, v)
if type(v) == "table" then
if not done[v] then
done[v] = true
local prefix = k .. "."
for k, v in pairs(v) do
flattenAndStore(prefix .. k, v)
end
end
elseif type(v) == "function" then
assert(uperms[k] == nil, "duplicate permanent value named " .. k)
-- If we have aliases its enough to store the value once.
if not perms[v] then
perms[v] = k
uperms[k] = v
end
end
end
for k, v in pairs(t) do
flattenAndStore(k, v)
end
end
store(_G)
store(...)
return perms, uperms

View File

@ -1,235 +0,0 @@
-- List of whitelisted globals, declared in advance because it's used by the
-- functions we declare in here, too, to avoid tampering.
local g
-- Until we get to ingame screens we log to Java's stdout. This is only used to
-- report internal failures during startup. Once we reach the kernel print is
-- replaced with a function that writes to the internal screen buffer.
do
local System, iprs, ts = java.require("java.lang.System"), ipairs, tostring
_G.print = function(...)
for _, value in iprs({...}) do
System.out:print(ts(value))
end
System.out:println()
end
end
print("test")
--[[
Install custom coroutine logic that forces coroutines to yield.
We replace the core functions: create, resume and yield.
- create is replaced with a function that periodically forces the created
coroutine to yield a string with the value "timeout".
- yield is replaced with a function that prepends any yielded values with a
nil value. This is purely to allow differentiating voluntary (normal)
yields from timeouts.
- resume is replaced with a function that checks the first value returned
from a yielding function. If we had a timeout we bubble upward, by also
yielding with a timeout. Otherwise normal yield functionality applies.
]]
--[[
do
-- Keep a backup of this function because it will be removed from our sandbox.
local create, resume, yield, unpack, sethook =
coroutine.create, coroutine.resume, coroutine.yield,
unpack, debug.sethook
-- This is the function we install as the hook.
local function check()
-- TODO check if there's a C stack frame? (i.e. we missed something)
yield("timeout")
end
-- Install our coroutine factory replacement which takes care of forcing the
-- created coroutines to yield once in a while. This is primarily used to to
-- avoid coroutines from blocking completely.
function _G.coroutine.create(f)
local co = create(f)
sethook(co, check, "", 100000)
return co
end
-- Replace yield function used from now on to be able to distinguish between
-- voluntary and forced yields.
function _G.coroutine.yield(...)
return yield(nil, ...)
end
-- Replace the resume function with one that automatically forwards timeouts.
function _G.coroutine.resume(...)
while true do
local result = {resume(...)}
if result[1] and result[2] == "timeout" then
return yield("timeout")
else
return result[1], unpack(result, 3)
end
end
end
end
]]
--[[ Set up the global environment we make available to userspace programs. ]]
g = {
-- Top level values. The selection of kept methods rougly follows the list
-- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes
-- Some entries have been kept although they are marked as unsafe on the
-- wiki, due to how we set up our environment: we clear the globals table,
-- so it does not matter if user-space functions gain access to the global
-- environment. We pretty much give all user-space code full control to
-- mess up the VM on the Lua side, we just want to make sure they can never
-- reach out to the Java side in an unintended way.
assert = assert,
error = error,
pcall = pcall,
xpcall = xpcall,
ipairs = ipairs,
next = next,
pairs = pairs,
rawequal = rawequal,
rawget = rawget,
rawset = rawset,
select = select,
unpack = unpack,
type = type,
tonumber = tonumber,
tostring = tostring,
-- Loadstring is OK because it's OK that the loaded chunk is in the global
-- environment as mentioned in the comment above.
loadstring = loadstring,
-- We don't care what users do with metatables. The only raised concern was
-- about breaking an environment, and we don't care about that.
getmetatable = getmetatable,
setmetatable = setmetatable,
-- Same goes for environment setters themselves. We do use local environments
-- for loaded scripts, but that's more for convenience than for control.
getfenv = getfenv,
setfenv = setfenv,
-- Custom print that actually writes to the screen buffer.
print = print,
coroutine = {
create = coroutine.create,
resume = coroutine.resume,
running = coroutine.running,
status = coroutine.status,
wrap = coroutine.wrap,
yield = coroutine.yield
},
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,
maxn = table.maxn,
remove = table.remove,
sort = table.sort,
--[[
Because I need this this every so often I decided to include it in the
base API. This allows copying tables shallow or deep, to a new table or
into an existing one. Usage:
table.copy(t) -- new table, shallow copy
table.copy(t, true) -- new table, deep copy
table.copy(t1, t2) -- copy t1 to t2, shallow copy
table.copy(t1, t2, true) -- copy t1 to t2, deep copy
]]
copy =
function(from, to, deep)
g.assert(g.type(from) == "table",
"bad argument #1 (table expected, got " .. g.type(from) .. ")")
deep = deep or (other and g.type(other) ~= "table")
local copied, shallowcopy, deepcopy = {}
function shallowcopy(from, to)
for k, v in g.pairs(from) do
to[k] = (deep and g.type(v) == "table") and deepcopy(v) or v
end
return to
end
function deepcopy(t)
if copied[t] then return copied[t] end
copied[t] = {}
return shallowcopy(t, copied[t])
end
return shallowcopy(from, g.type(to) == "table" and to or {})
end
},
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,
log10 = math.log10,
max = math.max,
min = math.min,
modf = math.modf,
pi = math.pi,
pow = math.pow,
rad = math.rad,
-- TODO Check if different Java LuaState's interfere via this. If so we
-- may have to create a custom random instance to replace the built
-- in random functionality of Lua.
random = math.random,
randomseed = math.randomseed,
sin = math.sin,
sinh = math.sinh,
sqrt = math.sqrt,
tan = math.tan,
tanh = math.tanh
},
os = {
clock = os.clock,
date = os.date,
difftime = os.difftime,
time = os.time,
-- TODO Evaluate whether this can actually get dangerous.
traceback = debug.traceback,
},
debug = debug
}
_G.table.copy = g.table.copy
do return end
-- Clear the globals table (except for its self-reference) and copy sandbox.
local copy = g.table.copy
for k, _ in pairs(_G) do
if k ~= "_G" then _G[k] = nil end
end
copy(g, _G)

View File

@ -31,20 +31,4 @@ public @interface Callback {
* The name under which the method will be available in Lua.
*/
String name();
/**
* Whether this API function is called synchronized with the server thread.
*
* Each computer runs its Lua state using a separate thread, to avoid slowing
* down the game or lock it up completely. This means that when a state calls
* a driver's API function this code is run in such a worker thread that is
* not synchronized with the server thread (i.e. the main game thread). The
* driver must therefore take care to avoid threading issues. For convenience
* a driver can request that an API function is only called in sync with the
* server thread. In that case the computer's thread will stop and wait for
* the server thread to come along. The server thread will see that the
* computer thread is waiting for it and perform it's API call for it, then
* let it loose again and continue with what it was doing.
*/
boolean synchronize() default false;
}

View File

@ -8,13 +8,11 @@ class Computer(val owner: AnyRef) extends IInternalComputerContext {
def start() = false
def stop() {}
def update() {}
def lock() {}
def unlock() {}
def signal(pid: Int, name: String, args: Any*) {}
def signal(name: String, args: Any*) {}
def readFromNBT(nbt: NBTTagCompound) {}

View File

@ -59,18 +59,13 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
override def createTileEntity(world: World, metadata: Int) = new TileEntityComputer(world.isRemote)
// ----------------------------------------------------------------------- //
// Block rotation
// Destruction / Interaction
// ----------------------------------------------------------------------- //
override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) {
if (!world.isRemote) {
val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3
setRotation(world, x, y, z, facing)
override def breakBlock(world: World, x: Int, y: Int, z: Int, `side?`: Int, metadata: Int) = {
world.getBlockTileEntity(x, y, z).asInstanceOf[TileEntityComputer].turnOff()
super.breakBlock(world, x, y, z, `side?`, metadata)
}
}
override def getValidRotations(world: World, x: Int, y: Int, z: Int) =
Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST)
override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer,
side: Int, hitX: Float, hitY: Float, hitZ: Float) = {
@ -85,6 +80,20 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
true
}
// ----------------------------------------------------------------------- //
// Block rotation
// ----------------------------------------------------------------------- //
override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) {
if (!world.isRemote) {
val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3
setRotation(world, x, y, z, facing)
}
}
override def getValidRotations(world: World, x: Int, y: Int, z: Int) =
Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST)
def rotation(world: IBlockAccess, x: Int, y: Int, z: Int) =
// Renderer(down, up, north, south, west, east) -> Facing(south, west, north, east) inverted.
Array(0, 0, 0, 2, 3, 1)(world.getBlockMetadata(x, y, z))

View File

@ -10,12 +10,10 @@ trait IInternalComputerContext extends IComputerContext {
def start(): Boolean
def stop(): Unit
def update()
def lock()
def unlock()
def readFromNBT(nbt: NBTTagCompound)
def writeToNBT(nbt: NBTTagCompound)

View File

@ -1,11 +1,21 @@
package li.cil.oc.common.tileentity
import li.cil.oc.server.computer.IComputerEnvironment
import java.util.concurrent.atomic.AtomicBoolean
import li.cil.oc.server.computer.IComputerEnvironment
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.tileentity.TileEntity
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.ForgeSubscribe
import net.minecraftforge.event.world.ChunkEvent
import net.minecraftforge.event.world.WorldEvent
class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnvironment {
def this() = this(false)
MinecraftForge.EVENT_BUS.register(this)
private val hasChanged = new AtomicBoolean()
// ----------------------------------------------------------------------- //
// General
// ----------------------------------------------------------------------- //
@ -16,17 +26,49 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv
def turnOn() = computer.start()
def turnOff() = computer.stop()
override def readFromNBT(nbt: NBTTagCompound) = {
super.readFromNBT(nbt)
computer.readFromNBT(nbt)
}
override def writeToNBT(nbt: NBTTagCompound) = {
println("SAVING")
super.writeToNBT(nbt)
computer.writeToNBT(nbt)
}
override def updateEntity() = computer.update()
override def updateEntity() = {
computer.update()
if (hasChanged.get())
worldObj.updateTileEntityChunkAndDoNothing(
this.xCoord, this.yCoord, this.zCoord, this)
}
// ----------------------------------------------------------------------- //
// Event Bus
// ----------------------------------------------------------------------- //
@ForgeSubscribe
def onChunkUnload(e: ChunkEvent.Unload) = {
println("CHUNK UNLOADING")
MinecraftForge.EVENT_BUS.unregister(this)
computer.stop()
}
@ForgeSubscribe
def onWorldUnload(e: WorldEvent.Unload) = {
println("WORLD UNLOADING")
MinecraftForge.EVENT_BUS.unregister(this)
computer.stop()
}
// ----------------------------------------------------------------------- //
// IComputerEnvironment
// ----------------------------------------------------------------------- //
def world = worldObj
def markAsChanged() = hasChanged.set(true)
}

View File

@ -1,12 +1,11 @@
package li.cil.oc.server.computer
import java.util.concurrent._
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import scala.Array.canBuildFrom
import scala.collection.JavaConversions._
import scala.util.Random
import com.naef.jnlua._
@ -19,24 +18,6 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
// General
// ----------------------------------------------------------------------- //
/** The internal Lua state. Only set while the computer is running. */
private var lua: LuaState = null
/**
* The base memory consumption of the kernel. Used to permit a fixed base
* memory for user-space programs even if the amount of memory the kernel
* uses changes over time (i.e. with future releases of the mod). This is set
* when starting up the computer.
*/
private var baseMemory = 0
/**
* The time when the computer was started. This is used for our custom
* implementation of os.clock(), which returns the amount of the time the
* computer has been running.
*/
private var timeStarted = 0.0
/**
* The current execution state of the computer. This is used to track how to
* resume the computers main thread, if at all, and whether to accept new
@ -44,6 +25,17 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
*/
private var state = State.Stopped
/** The internal Lua state. Only set while the computer is running. */
private var lua: LuaState = null
/**
* The base memory consumption of the kernel. Used to permit a fixed base
* memory for userland even if the amount of memory the kernel uses changes
* over time (i.e. with future releases of the mod). This is set when
* starting up the computer.
*/
private var kernelMemory = 0
/**
* The queue of signals the Lua state should process. Signals are queued from
* the Java side and processed one by one in the Lua VM. They are the only
@ -52,22 +44,14 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
*/
private val signals = new LinkedBlockingQueue[Signal](256)
/**
* This is used to keep track of the current executor of the Lua state, for
* example to wait for the computer to finish running a task. This is used to
* cancel scheduled execution when a new signal arrives and to wait for the
* computer to shut down.
*/
private var future: Future[_] = null
// ----------------------------------------------------------------------- //
/**
* This lock is used by the thread executing the Lua state when it performs
* a synchronized API call. In that case it acquires this lock and waits for
* the server thread. The server thread will try to acquire the lock after
* notifying the state thread, to make sure the call was complete before
* resuming.
* The time (world time) when the computer was started. This is used for our
* custom implementation of os.clock(), which returns the amount of the time
* the computer has been running.
*/
private val driverLock = new ReentrantLock()
private var timeStarted = 0L
/**
* The last time (system time) the update function was called by the server
@ -76,42 +60,67 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
*/
private var lastUpdate = 0L
/**
* The current world time. This is used for our custom implementation of
* os.time(). This is updated by the server thread and read by the computer
* thread, to avoid computer threads directly accessing the world state.
*/
private var worldTime = 0L
// ----------------------------------------------------------------------- //
/**
* This is used to keep track of the current executor of the Lua state, for
* example to wait for the computer to finish running a task.
*/
private var future: Future[_] = null
/**
* The object our executor thread waits on if the last update has been a
* while, and the update function calls notify on each time it is run.
*/
private val updateMonitor = new Object()
private val pauseMonitor = new Object()
/** This is used to synchronize access to the state field. */
private val stateMonitor = new Object()
// ----------------------------------------------------------------------- //
// State
// ----------------------------------------------------------------------- //
/** Starts asynchronous execution of this computer if it isn't running. */
def start(): Boolean = state match {
case State.Stopped => {
if (init()) {
state = State.Running
def start(): Boolean = stateMonitor.synchronized(
state == State.Stopped && init() && {
state = State.Suspended
// Inject a dummy signal so that real one don't get swallowed. This way
// we can just ignore the parameters the first time the kernel is run.
signal("dummy")
future = Executor.pool.submit(this)
true
}
else false
}
case _ => false
}
})
/** Stops a computer asynchronously. */
def stop(): Unit = if (state != State.Stopped) {
signals.clear()
signal(0, "terminate")
/** Stops a computer, possibly asynchronously. */
def stop(): Unit = stateMonitor.synchronized {
if (state != State.Stopped) {
if (state != State.Running) {
// If the computer is not currently running we can simply close it,
// and cancel any pending future - which may already be running and
// waiting for the stateMonitor, so we do a hard abort.
if (future != null) {
future.cancel(true)
}
close()
}
else {
// Otherwise we enter an intermediate state to ensure the executor
// truly stopped, before switching back to stopped to allow starting
// the computer again. The executor will check for this state and
// call close.
state = State.Stopping
// Make sure the thread isn't waiting for an update.
pauseMonitor.synchronized(pauseMonitor.notify())
}
}
/** Stops a computer synchronously. */
def stopAndWait(): Unit = {
stop()
// Get a local copy to avoid having to synchronize it between the null
// check and the actual wait.
val future = this.future
if (future != null) future.get()
}
// ----------------------------------------------------------------------- //
@ -121,84 +130,102 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
def luaState = lua
def update() {
updateMonitor.synchronized {
if (state == State.Stopped) return
// Check if executor is waiting for a lock to interact with a driver.
future.synchronized {
if (state == State.Synchronizing) {
// Thread is waiting to perform synchronized API call, notify it.
future.notify()
// Wait until the API call completed, which is when the driver lock
// becomes available again (we lock it in the executor thread before
// waiting to be notified). We need an extra lock for that because the
// driver will release the lock on 'future' to do so (see lock()).
driverLock.lock()
driverLock.unlock()
stateMonitor.synchronized(state match {
case State.Stopped | State.Stopping => return
case State.DriverCall => {
assert(lua.getTop() == 2)
assert(lua.`type`(1) == LuaType.THREAD)
assert(lua.`type`(2) == LuaType.FUNCTION)
lua.resume(1, 1)
assert(lua.getTop() == 2)
assert(lua.`type`(2) == LuaType.TABLE)
state = State.DriverReturn
future = Executor.pool.submit(this)
}
case _ => /* nothing special to do */
})
// Remember when we started the computer for os.clock(). We do this in the
// update because only then can we be sure the world is available.
if (timeStarted == 0)
timeStarted = owner.world.getWorldInfo().getWorldTotalTime()
// Update world time for computer threads.
worldTime = owner.world.getWorldInfo().getWorldTotalTime()
if (worldTime % 40 == 0) {
signal("test", "ha!")
}
// Update last time run to let our executor thread know it doesn't have to
// pause, and wake it up if it did pause (because the game was paused).
lastUpdate = System.currentTimeMillis()
updateMonitor.notify()
}
lastUpdate = System.currentTimeMillis
// Tell the executor thread it may continue if it's waiting.
pauseMonitor.synchronized(pauseMonitor.notify())
}
def signal(pid: Int, name: String, args: Any*) = {
def signal(name: String, args: Any*) = {
args.foreach {
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
case _ => throw new IllegalArgumentException()
}
if (state != State.Stopped) {
signals.offer(new Signal(pid, name, Array(args)))
// TODO cancel delayed future and schedule for immediate execution
// if (this.synchronized(!signals.isEmpty() && state == State.Stopped)) {
// state = State.Running
// Executor.pool.execute(this)
// }
}
stateMonitor.synchronized(state match {
// We don't push new signals when stopped or shutting down.
case State.Stopped | State.Stopping =>
// Currently sleeping. Cancel that and start immediately.
case State.Sleeping =>
future.cancel(true)
state = State.Suspended
signals.offer(new Signal(name, args.toArray))
future = Executor.pool.submit(this)
// Running or in driver call or only a short yield, just push the signal.
case _ =>
signals.offer(new Signal(name, args.toArray))
})
}
def lock() {
driverLock.lock()
future.synchronized {
state = State.Synchronizing
future.wait()
}
def readFromNBT(nbt: NBTTagCompound): Unit = this.synchronized {
// Clear out what we currently have, if anything.
stateMonitor.synchronized {
assert(state != State.Running) // Lock on 'this' should guarantee this.
stop()
}
def unlock() {
driverLock.unlock()
}
def readFromNBT(nbt: NBTTagCompound): Unit = {
// If we're running we wait for the executor to yield, to get the Lua state
// into a valid, suspended state before trying to unpersist into it.
this.synchronized {
state = State(nbt.getInteger("state"))
if (state != State.Stopped && (lua != null || init())) {
baseMemory = nbt.getInteger("baseMemory")
timeStarted = nbt.getDouble("timeStarted")
if (state != State.Stopped && init()) {
// Unlimit memory use while unpersisting.
val memory = lua.getTotalMemory()
lua.setTotalMemory(Integer.MAX_VALUE)
val kernel = nbt.getString("kernel")
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist)
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
lua.pushString(kernel)
lua.call(2, 1)
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
lua.setTotalMemory(memory)
try {
// Try unpersisting Lua, because that's what all of the rest depends on.
// Clear the stack (meaning the current kernel).
lua.setTop(0)
signals.clear()
if (!unpersist(nbt.getByteArray("kernel")) || !lua.isThread(1)) {
// This shouldn't really happen, but there's a chance it does if
// the save was corrupt (maybe someone modified the Lua files).
throw new IllegalStateException("Could not restore kernel.")
}
if (state == State.DriverCall || state == State.DriverReturn) {
if (!unpersist(nbt.getByteArray("stack")) ||
(state == State.DriverCall && !lua.isFunction(2)) ||
(state == State.DriverReturn && !lua.isTable(2))) {
// Same as with the above, should not really happen normally, but
// could for the same reasons.
throw new IllegalStateException("Could not restore driver call.")
}
}
assert(signals.size() == 0)
val signalsTag = nbt.getTagList("signals")
signals.addAll((0 until signalsTag.tagCount()).
map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]).
map(signal => {
val argsTag = signal.getCompoundTag("args")
val argsLength = argsTag.getInteger("length")
new Signal(signal.getInteger("pid"), signal.getString("name"),
new Signal(signal.getString("name"),
(0 until argsLength).map("arg" + _).map(argsTag.getTag(_)).map {
case tag: NBTTagByte => tag.data
case tag: NBTTagShort => tag.data
@ -210,40 +237,59 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
}.toArray)
}))
timeStarted = nbt.getLong("timeStarted")
// Start running our worker thread.
assert(future == null)
future = Executor.pool.submit(this)
}
catch {
case t: Throwable => {
t.printStackTrace()
// TODO display error in-game on monitor or something
//signal("crash", "memory corruption")
close()
}
}
finally if (lua != null) {
// Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0)
// Start running our worker thread if we don't already have one.
if (future == null) future = Executor.pool.submit(this)
lua.setTotalMemory(memory)
}
}
}
def writeToNBT(nbt: NBTTagCompound): Unit = {
// If we're running we wait for the executor to yield, to get the Lua state
// into a valid, suspended state before trying to persist it.
this.synchronized {
def writeToNBT(nbt: NBTTagCompound): Unit = this.synchronized {
stateMonitor.synchronized {
assert(state != State.Running) // Lock on 'this' should guarantee this.
assert(state != State.Stopping) // Only set while executor is running.
}
nbt.setInteger("state", state.id)
if (state == State.Stopped) return
if (state == State.Stopped) {
return
}
nbt.setInteger("baseMemory", baseMemory)
nbt.setDouble("timeStarted", timeStarted)
// Call pluto.persist(persistTable, _G) and store the string result.
// Unlimit memory while persisting.
val memory = lua.getTotalMemory()
lua.setTotalMemory(Integer.MAX_VALUE)
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persist)
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
lua.call(2, 1)
val kernel = lua.toString(-1)
lua.pop(1)
nbt.setString("kernel", kernel)
lua.setTotalMemory(memory)
try {
// Try persisting Lua, because that's what all of the rest depends on.
// While in a driver call we have one object on the global stack: either
// the function to call the driver with, or the result of the call.
if (state == State.DriverCall || state == State.DriverReturn) {
assert(
if (state == State.DriverCall) lua.`type`(2) == LuaType.FUNCTION
else lua.`type`(2) == LuaType.TABLE)
nbt.setByteArray("stack", persist())
}
// Save the kernel state (which is always at stack index one).
assert(lua.`type`(1) == LuaType.THREAD)
nbt.setByteArray("kernel", persist())
val list = new NBTTagList()
for (s <- signals.iterator()) {
val signal = new NBTTagCompound()
signal.setInteger("pid", s.pid)
signal.setString("name", s.name)
// TODO Test with NBTTagList, but supposedly it only allows entries
// with the same type, so I went with this for now...
@ -263,35 +309,66 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
}
nbt.setTag("signals", list)
lua.gc(LuaState.GcAction.COLLECT, 0)
nbt.setLong("timeStarted", timeStarted)
}
catch {
case t: Throwable => {
t.printStackTrace()
nbt.setInteger("state", State.Stopped.id)
}
}
finally {
// Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0)
lua.setTotalMemory(memory)
}
}
private def persist(): Array[Byte] = {
lua.getGlobal("persist") // ... obj persist?
if (lua.`type`(-1) == LuaType.FUNCTION) { // ... obj persist
lua.pushValue(-2) // ... obj persist obj
lua.call(1, 1) // ... obj str?
if (lua.`type`(-1) == LuaType.STRING) { // ... obj str
val result = lua.toByteArray(-1)
lua.pop(1) // ... obj
return result
} // ... obj :(
} // ... obj :(
lua.pop(1) // ... obj
return Array[Byte]()
}
private def unpersist(value: Array[Byte]): Boolean = {
lua.getGlobal("unpersist") // ... unpersist?
if (lua.`type`(-1) == LuaType.FUNCTION) { // ... unpersist
lua.pushByteArray(value) // ... unpersist str
lua.call(1, 1) // ... obj
return true
} // ... :(
return false
}
def init(): Boolean = {
// Creates a new state with all base libraries as well as the Pluto
// library loaded into it. This means the state has much more power than
// it rightfully should have, so we sandbox it a bit in the following.
// Creates a new state with all base libraries and the persistence library
// loaded into it. This means the state has much more power than it
// rightfully should have, so we sandbox it a bit in the following.
lua = LuaStateFactory.createState()
try {
// Before doing the actual sandboxing we save the Pluto library into the
// registry, since it'll be removed from the globals table.
lua.getGlobal("eris")
lua.getField(-1, "persist")
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persist)
lua.getField(-1, "unpersist")
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist)
lua.pop(1)
// If something went wrong while creating the state there's nothing else
// we can do here...
if (lua == null) return false
try {
// Push a couple of functions that override original Lua API functions or
// that add new functionality to it.
// that add new functionality to it.1)
lua.getGlobal("os")
// Return ingame time for os.time().
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
// Minecraft starts days at 6 o'clock, so we add six hours.
lua.pushNumber((owner.world.getTotalWorldTime() + 6000.0) / 1000.0)
// Minecraft starts days at 6 o'clock, so we add those six hours.
lua.pushNumber(worldTime + 6000)
return 1
}
})
@ -301,72 +378,146 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
// been running, instead of the native library...
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
lua.pushNumber(owner.world.getTotalWorldTime() - timeStarted)
// World time is in ticks, and each second has 20 ticks. Since we
// want os.clock() to return real seconds, though, we'll divide it
// accordingly.
lua.pushNumber((owner.world.getTotalWorldTime() - timeStarted) / 20.0)
return 1
}
})
lua.setField(-2, "clock")
// TODO Other overrides?
// Custom os.difftime(). For most Lua implementations this would be the
// same anyway, but just to be on the safe side.
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
val t2 = lua.checkNumber(1)
val t1 = lua.checkNumber(2)
lua.pushNumber(t2 - t1)
return 1
}
})
lua.setField(-2, "difftime")
// Allow the system to read how much memory it uses and has available.
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
lua.pushInteger(lua.getTotalMemory() - kernelMemory)
return 1
}
})
lua.setField(-2, "totalMemory")
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
lua.pushInteger(lua.getFreeMemory())
return 1
}
})
lua.setField(-2, "freeMemory")
// Pop the os table.
lua.pop(1)
// Run the sandboxing script. This script is presumed to be under our
// control. We do the sandboxing in Lua because it'd be a pain to write
// using only stack operations...
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/sandbox.lua"), "sandbox", "t")
lua.call(0, 0)
lua.getGlobal("math")
// Install all driver callbacks into the registry. This is done once in
// the beginning so that we can take the memory the callbacks use into
// account when computing the kernel's memory use, as well as for building
// a table of permanent values used when persisting/unpersisting the state.
Drivers.injectInto(this)
// Run the script that builds the tables with permanent values. These
// tables must contain all java callbacks (i.e. C functions, since they
// are wrapped on the native side using a C function, of course). They
// are used when persisting/unpersisting the state so that Pluto knows
// which values it doesn't have to serialize (since it cannot persist C
// functions). We store the two tables in the registry.
// TODO These tables may change after loading a game, for example due to
// a new mod being installed or an old one being removed. In that case,
// previously existing values will "suddenly" become nil. We may want to
// consider detecting such changes and rebooting computers in that case.
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/persistence.lua"), "persistence", "t")
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
// We give each Lua state it's own randomizer, since otherwise they'd
// use the good old rand() from C. Which can be terrible, and isn't
// necessarily thread-safe.
val random = new Random
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
println(lua.toString(1))
lua.getTop() match {
case 0 => lua.pushNumber(random.nextDouble)
case 1 => {
val u = lua.checkInteger(1)
lua.checkArg(1, 1 < u, "interval is empty")
lua.pushInteger(1 + random.nextInt(u))
}
case 2 => {
val l = lua.checkInteger(1)
val u = lua.checkInteger(2)
lua.checkArg(1, l < u, "interval is empty")
lua.pushInteger(l + random.nextInt(u - l))
}
case _ => throw new IllegalArgumentException("wrong number of arguments")
}
return 1
}
})
lua.setField(-2, "random")
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
val seed = lua.checkInteger(1)
random.setSeed(seed)
return 0
}
})
lua.call(2, 2)
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
lua.setField(-2, "randomseed")
// Pop the math table.
lua.pop(1)
// Until we get to ingame screens we log to Java's stdout.
lua.pushJavaFunction(new JavaFunction() {
def invoke(lua: LuaState): Int = {
for (i <- 1 to lua.getTop()) {
lua.`type`(i) match {
case LuaType.NIL => print("nil")
case LuaType.BOOLEAN => print(lua.toBoolean(i))
case LuaType.NUMBER => print(lua.toNumber(i))
case LuaType.STRING => print(lua.toString(i))
case LuaType.TABLE => print("table")
case LuaType.FUNCTION => print("function")
case LuaType.THREAD => print("thread")
case LuaType.LIGHTUSERDATA | LuaType.USERDATA => print("userdata")
}
}
return 0
}
})
lua.setGlobal("print")
// TODO Other overrides?
// Install all driver callbacks into the state. This is done once in
// the beginning so that we can take the memory the callbacks use into
// account when computing the kernel's memory use, as well as for
// building a table of permanent values used when persisting/unpersisting
// the state.
lua.newTable()
lua.setGlobal("drivers")
Drivers.injectInto(this)
// Run the boot script. This creates the global sandbox variable that is
// used as the environment for any processes the kernel spawns, adds a
// couple of library functions and sets up the permanent value tables as
// well as making the functions used for persisting/unpersisting
// available as globals.
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/boot.lua"), "boot", "t")
lua.call(0, 0)
// Load the basic kernel which takes care of handling signals by managing
// the list of active processes. Whatever functionality we can we implement
// in Lua, so we also implement most of the kernel's functionality in Lua.
// Why? Because like this it's automatically persisted for us without
// having to write more additional NBT stuff.
// the list of active processes. Whatever functionality we can we
// implement in Lua, so we also implement most of the kernel's
// functionality in Lua. Why? Because like this it's automatically
// persisted for us without having to write more additional NBT stuff.
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/kernel.lua"), "kernel", "t")
lua.newThread()
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
lua.newThread() // Leave it as the first value on the stack.
// Run the garbage collector to get rid of stuff left behind after the
// initialization phase to get a good estimate of the base memory usage
// the kernel has. We remember that size to grant user-space programs a
// fixed base amount of memory.
// fixed base amount of memory, regardless of the memory need of the
// underlying system (which may change across releases).
lua.gc(LuaState.GcAction.COLLECT, 0)
baseMemory = lua.getTotalMemory() - lua.getFreeMemory()
lua.setTotalMemory(baseMemory + 128 * 1024)
kernelMemory = lua.getTotalMemory() - lua.getFreeMemory()
lua.setTotalMemory(kernelMemory + 64 * 1024)
// Remember when we started the computer.
timeStarted = System.currentTimeMillis()
println("Kernel uses " + (kernelMemory / 1024) + "KB of memory.")
println("Kernel uses " + baseMemory + " bytes of memory.")
// Clear any left-over signals from a previous run.
signals.clear()
return true
}
@ -379,145 +530,151 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
return false
}
def close() {
def close(): Unit = stateMonitor.synchronized(
if (state != State.Stopped) {
state = State.Stopped
lua.setTotalMemory(Integer.MAX_VALUE);
lua.close()
lua = null
baseMemory = 0
timeStarted = 0
state = State.Stopped
future = null
kernelMemory = 0
signals.clear()
timeStarted = 0
future = null
})
// This is a really high level lock that we only use for saving and loading.
def run(): Unit = this.synchronized {
println(" > executor enter")
val driverReturn = State.DriverReturn == stateMonitor.synchronized {
val oldState = state
state = State.Running
oldState
}
def run() {
try {
println("start running computer")
// See if the game appears to be paused, in which case we also pause.
if (System.currentTimeMillis() - lastUpdate > 500)
updateMonitor.synchronized {
updateMonitor.wait()
}
println("running computer")
if (System.currentTimeMillis - lastUpdate > 500)
pauseMonitor.synchronized(pauseMonitor.wait())
// This is synchronized so that we don't run a Lua state while saving or
// loading the computer to or from an NBTTagCompound.
this.synchronized {
// Push the kernel coroutine onto the stack so that we can resume it.
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
// Get a copy to check the coroutine's status after it ran.
lua.pushValue(-1)
// loading the computer to or from an NBTTagCompound or other stuff
// corrupting our Lua state.
// The kernel thread will always be at stack index one.
assert(lua.`type`(1) == LuaType.THREAD)
try {
// Resume the Lua state and remember the number of results we get.
val results = state match {
// Current coroutine was forced to yield. Resume without injecting any
// signals. Any passed arguments would simply be ignored.
case State.Yielding => {
println("resuming forced yield")
lua.resume(-1, 0)
val results = if (driverReturn) {
// If we were doing a driver call, continue where we left off.
assert(lua.getTop() == 2)
lua.resume(1, 1)
}
// We're running normally, i.e. all coroutines yielded voluntarily and
// this yield comes directly out of the main kernel coroutine.
case _ => {
// Try to get a signal to run the state with.
signals.poll() match {
else signals.poll() match {
// No signal, just run any non-sleeping processes.
case null => {
println("resuming without signal")
lua.resume(-1, 0)
}
case null => lua.resume(1, 0)
// Got a signal, inject it and call any handlers (if any).
case signal => {
println("injecting signal")
lua.pushInteger(signal.pid)
lua.pushString(signal.name)
signal.args.foreach {
case arg: Byte => lua.pushInteger(arg)
case arg: Short => lua.pushInteger(arg)
case arg: Integer => lua.pushInteger(arg)
case arg: Int => lua.pushInteger(arg)
case arg: Long => lua.pushNumber(arg)
case arg: Float => lua.pushNumber(arg)
case arg: Double => lua.pushNumber(arg)
case arg: String => lua.pushString(arg)
}
lua.resume(-1, 2 + signal.args.length)
}
}
lua.resume(1, 1 + signal.args.length)
}
}
println("lua yielded")
// State has inevitably changed, mark as changed to save again.
owner.markAsChanged()
// Only queue for next execution step if the kernel is still alive.
if (lua.status(-(results + 1)) != 0) {
// See what we have. The convention is that if the first result is a
// string with the value "timeout" the currently running coroutines was
// forced to yield by the execution limit (i.e. the yield comes from the
// debug hook we installed as seen in the sandbox.lua script). Otherwise
// it's a normal yield, and we get the time to wait before we should try
// to execute the state again in seconds.
if (lua.isString(-results) && "timeout".equals(lua.toString(-results))) {
// Forced yield due to long execution time. Remember this for the next
// time we run, so we don't try to insert a signal which would get
// ignored.
state = State.Yielding
future = Executor.pool.submit(this)
if (lua.status(1) == LuaState.YIELD) {
// Lua state yielded normally, see what we have.
stateMonitor.synchronized {
if (state == State.Stopping) {
// Someone called stop() in the meantime.
close()
}
else {
// Lua state yielded normally, see how long we should wait before
else if (results == 1 && lua.isNumber(2)) {
// We got a number. This tells us how long we should wait before
// resuming the state again.
val sleep = (lua.toNumber(-1) * 1000).toLong
state = State.Running
val sleep = (lua.toNumber(2) * 1000).toLong
lua.pop(results)
state = State.Sleeping
future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS)
}
else if (results == 1 && lua.isFunction(2)) {
// If we get one function it's a wrapper for a driver call.
state = State.DriverCall
future = null
}
else {
// Something else, just pop the results and try again.
lua.pop(results)
state = State.Suspended
future = Executor.pool.submit(this)
}
}
println(" < executor leave")
// Avoid getting to the closing part after the exception handling.
return
}
}
catch {
// The kernel should never throw. If it does, the computer crashed
// hard, so we just close the state.
// TODO Print something to an in-game screen, a la kernel panic.
case ex: LuaRuntimeException => ex.printLuaStackTrace()
case ex: Throwable => ex.printStackTrace()
case er: LuaMemoryAllocationException => {
// This is pretty likely to happen for non-upgraded computers.
// TODO Print an error message to an in-game screen.
println("Out of memory!")
er.printStackTrace()
}
println("free memory: " + lua.getFreeMemory())
// If the kernel is no longer running the computer has stopped.
lua.status(-1) match {
case LuaState.YIELD => lua.pop(1)
case _ => updateMonitor.synchronized(close())
}
}
println("end running computer")
}
catch {
// Top-level catch-anything, because otherwise those exceptions get
// gobbled up by the executor unless we call the future's get().
case t: Throwable => t.printStackTrace()
}
// If we come here there was an error or we stopped, kill off the state.
close()
println(" < executor leave")
}
/** Signals are messages sent to the Lua state's processes from Java. */
private class Signal(val pid: Int, val name: String, val args: Array[Any]) {
}
/** Signals are messages sent to the Lua state from Java asynchronously. */
private class Signal(val name: String, val args: Array[Any])
/** Possible states of the computer, and in particular its executor. */
private object State extends Enumeration {
/** Self explanatory: the computer is not running right now. */
/** The computer is not running right now and there is no Lua state. */
val Stopped = Value("Stopped")
/** The computer is running but yielded for a moment. */
val Suspended = Value("Suspended")
/** The computer is running but yielding for a longer amount of time. */
val Sleeping = Value("Sleeping")
/** The computer is up and running, executing Lua code. */
val Running = Value("Running")
/** The computer is yielding because of its execution limit. */
val Yielding = Value("Yielding")
/** The computer is currently shutting down (waiting for executor). */
val Stopping = Value("Stopping")
/** The computer executor is waiting for the server thread. */
val Synchronizing = Value("Synchronizing")
/** The computer executor is waiting for a driver call to be made. */
val DriverCall = Value("DriverCall")
/** The computer should resume with the result of a driver call. */
val DriverReturn = Value("DriverReturn")
}
/** Singleton for requesting executors that run our Lua states. */
@ -532,9 +689,9 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
def newThread(r: Runnable): Thread = {
val name = "OpenComputers-" + threadNumber.getAndIncrement()
val thread = new Thread(group, r, name, 0)
if (thread.isDaemon())
thread.setDaemon(false)
val thread = new Thread(group, r, name)
if (!thread.isDaemon())
thread.setDaemon(true)
if (thread.getPriority() != Thread.MIN_PRIORITY)
thread.setPriority(Thread.MIN_PRIORITY)
return thread
@ -542,13 +699,3 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
})
}
}
/** Names of entries in the registries of the Lua states of computers. */
private[computer] object ComputerRegistry {
val kernel = "oc_kernel"
val driverApis = "oc_apis"
val persist = "oc_persist"
val unpersist = "oc_unpersist"
val unpersistTable = "oc_unpersistTable"
val persistTable = "oc_persistTable"
}

View File

@ -2,8 +2,6 @@ package li.cil.oc.server.computer
import java.lang.reflect.Method
import scala.Array.canBuildFrom
import com.naef.jnlua.JavaFunction
import com.naef.jnlua.LuaState
@ -18,115 +16,49 @@ private[oc] class Driver(val driver: IDriver) {
if (api == null || api.isEmpty()) return
val lua = context.luaState
// Get or create registry entry holding API tables.
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
if (lua.isNil(-1)) {
lua.pop(1)
lua.newTable()
lua.pushValue(-1)
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
}
// Get or create table holding API tables.
lua.getGlobal("drivers") // ... drivers?
assert(!lua.isNil(-1)) // ... drivers
// Get or create API table.
lua.getField(-1, api)
if (lua.isNil(-1)) {
lua.pop(1)
lua.newTable()
lua.pushValue(-1)
lua.setField(-3, api)
}
lua.getField(-1, api) // ... drivers api?
if (lua.isNil(-1)) { // ... drivers nil
lua.pop(1) // ... drivers
lua.newTable() // ... drivers api
lua.pushValue(-1) // ... drivers api api
lua.setField(-3, api) // ... drivers api
} // ... drivers api
for (method <- driver.getClass().getMethods()) {
val annotation = method.getAnnotation(classOf[Callback])
if (annotation != null) {
for (method <- driver.getClass().getMethods())
method.getAnnotation(classOf[Callback]) match {
case null => Unit // No annotation.
case annotation => {
val name = annotation.name
lua.getField(-1, name)
if (lua.isNil(-1)) {
lua.getField(-1, name) // ... drivers api func?
if (lua.isNil(-1)) { // ... drivers api nil
// No such entry yet.
lua.pop(1) // ... drivers api
lua.pushJavaFunction(new MethodWrapper(context, method)) // ... drivers api func
lua.setField(-2, name) // ... drivers api
}
else { // ... drivers api func
// Entry already exists, skip it.
lua.pop(1)
lua.pop(1) // ... drivers api
// TODO Log warning properly via a logger.
println("WARNING: Duplicate API entry, ignoring: " + api + "." + name)
}
else {
// No such entry yet. Pop the nil and build our callback wrapper.
lua.pop(1)
if (annotation.synchronize) {
lua.pushJavaFunction(new SynchronizedMethodWrapper(context, method))
}
else {
lua.pushJavaFunction(new MethodWrapper(context, method))
}
lua.setField(-2, name)
}
}
} // ... drivers api
lua.pop(2) // ...
}
// Pop the API table and the table holding all APIs.
lua.pop(2)
}
/**
* This installs the driver on the computer, providing an API to interact
* with the device.
*
* This copies an existing API table from the registry and executes any
* initialization code provided by the driver.
*/
def install(context: IInternalComputerContext) {
copyAPI(context)
// Do we have custom initialization code?
val code = driver.apiCode
if (code != null && !code.isEmpty()) {
val lua = context.luaState
lua.load(code, driver.componentName)
// TODO Set environment so that variables not explicitly added to globals
// table won't accidentally clutter it.
lua.call(0, 0)
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
def invoke(state: LuaState): Int = {
return 0
}
}
private def copyAPI(context: IInternalComputerContext) {
// Check if the component actually provides an API.
val api = driver.apiName
if (api == null && api.isEmpty()) return
// Get the Lua state and check if the API table already exists.
val lua = context.luaState
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
if (lua.isNil(-1)) {
// We don't have any APIs at all.
lua.pop(1)
return
}
lua.getField(-1, api)
if (lua.isNil(-1)) {
// No such API. Which is kind of weird, but hey.
lua.pop(2)
return
}
// OK, we have our registry table. Create a new table to copy into.
val registryTable = lua.getTop()
lua.newTable()
val globalTable = lua.getTop()
// Copy all keys (which are the API functions).
lua.pushNil()
while (lua.next(registryTable)) {
val key = lua.toString(-2)
lua.setField(globalTable, key)
}
// Push our globals table into the global name space.
lua.setGlobal(api)
// Pop the registry API table and registry table holding all API tables.
lua.pop(2)
}
/*
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
private val classOfBoolean = classOf[Boolean]
private val classOfByte = classOf[Byte]
@ -187,12 +119,5 @@ private[oc] class Driver(val driver: IDriver) {
method.invoke(driver, args)
}
}
private class SynchronizedMethodWrapper(context: IInternalComputerContext, method: Method) extends MethodWrapper(context, method) {
override def call(args: AnyRef*) = {
context.lock()
try super.call(args)
finally context.unlock()
}
}
*/
}

View File

@ -5,5 +5,5 @@ import com.naef.jnlua.LuaState
import net.minecraft.nbt.NBTTagCompound
trait IComputerContext {
def signal(pid: Int, name: String, args: Any*)
def signal(name: String, args: Any*)
}

View File

@ -8,4 +8,7 @@ import net.minecraft.world.World
*/
trait IComputerEnvironment {
def world: World
/** Called when the computer state changed, so it should be saved again. */
def markAsChanged(): Unit
}

View File

@ -88,7 +88,29 @@ private[computer] object LuaStateFactory {
val state = new LuaState(Integer.MAX_VALUE)
try {
// Load all libraries.
state.openLibs()
state.openLib(LuaState.Library.BASE)
state.openLib(LuaState.Library.BIT32)
state.openLib(LuaState.Library.COROUTINE)
state.openLib(LuaState.Library.DEBUG)
state.openLib(LuaState.Library.ERIS)
state.openLib(LuaState.Library.MATH)
state.openLib(LuaState.Library.STRING)
state.openLib(LuaState.Library.TABLE)
state.pop(8)
// Prepare table for os stuff.
state.newTable()
state.setGlobal("os")
// Remove some other functions we don't need and are dangerous.
state.pushNil()
state.setGlobal("dofile")
state.pushNil()
state.setGlobal("loadfile")
state.pushNil()
state.setGlobal("module")
state.pushNil()
state.setGlobal("require")
}
catch {
case ex: Throwable => {