mirror of
https://github.com/MightyPirates/OpenComputers.git
synced 2025-09-12 16:57:32 -04:00
resumable computers; signals appear to work; fixed bug in allocator in jnlua
This commit is contained in:
parent
4e820da1f2
commit
0327cec937
Binary file not shown.
Binary file not shown.
210
assets/opencomputers/lua/boot.lua
Normal file
210
assets/opencomputers/lua/boot.lua
Normal 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
|
@ -1,155 +1,339 @@
|
|||||||
--[[
|
--[[ Basic functionality, drives userland and enforces timeouts.
|
||||||
Basic OS functionality, such as launching new programs and loading drivers.
|
|
||||||
|
|
||||||
This is called as the main coroutine by the computer. If this throws, the
|
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.
|
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()
|
local function main()
|
||||||
--[[
|
--[[ Create the sandbox as a thread-local variable so it is persisted. ]]
|
||||||
A copy the globals table to avoid user-space programs messing with us. The
|
local sandbox = buildSandbox()
|
||||||
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
|
|
||||||
|
|
||||||
-- List of all active processes.
|
--[[ List of signal handlers, by name. ]]
|
||||||
local processes = {}
|
local signals = setmetatable({}, {__mode = "v"})
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
--[[ Registers or unregisters a callback for a signal. ]]
|
--[[ Registers or unregisters a callback for a signal. ]]
|
||||||
function _G.os.signal(name, callback)
|
function sandbox.os.signal(name, callback)
|
||||||
assert(g.type(name) == "string" and g.type(callback) == "function",
|
checkType(1, name, "string")
|
||||||
g.string.format("'string', 'function' expected, got '%s', '%s'",
|
checkType(2, callback, "function", "nil")
|
||||||
g.type(name), g.type(callback)))
|
|
||||||
processes[currentProcess][name] = callback
|
local oldCallback = signals[name]
|
||||||
|
signals[name] = callback
|
||||||
|
return oldCallback
|
||||||
end
|
end
|
||||||
|
|
||||||
-- We replace the default yield function so that be can differentiate between
|
--[[ Error handler for signal callbacks. ]]
|
||||||
-- process level yields and coroutine level yields - in case a process spawns
|
local function onSignalError(msg)
|
||||||
-- new coroutines.
|
if type(msg) == "table" then
|
||||||
--[[
|
if msg.timeout then
|
||||||
function _G.coroutine.yield(...)
|
msg = "too long without yielding"
|
||||||
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
|
|
||||||
|
|
||||||
else
|
else
|
||||||
return result[1], g.select(3, g.unpack(result))
|
msg = msg.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
print(msg)
|
||||||
|
return msg
|
||||||
end
|
end
|
||||||
]]
|
|
||||||
|
|
||||||
-- Create the actual copy now that we have our copying function.
|
--[[ Set up the shell, which is really just a Lua interpreter. ]]
|
||||||
g = g.table.copy(g, true)
|
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.
|
local i = 0
|
||||||
g.os.execute(function() print("hi") os.sleep(5) end)
|
while true do
|
||||||
|
i = i + 1
|
||||||
|
print("ping " .. i)
|
||||||
|
os.sleep(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
shellThread = coroutine.create(shell)
|
||||||
|
end
|
||||||
|
startShell()
|
||||||
|
|
||||||
print("Running kernel...")
|
print("Running kernel...")
|
||||||
|
|
||||||
while true do
|
-- Pending signal to be processed. We only either process a signal *or* run
|
||||||
print("ping")
|
-- the shell, to keep the chance of long blocks low (and "accidentally" going
|
||||||
g.coroutine.yield(5)
|
-- over the timeout).
|
||||||
end
|
local signal
|
||||||
|
while coroutine.status(shellThread) ~= "dead" do
|
||||||
-- Begin running our processes. We run all processes consecutively if they are
|
deadline = os.clock() + 5
|
||||||
-- currently "awake", meaning not waiting for a call to os.sleep to return. If
|
local result
|
||||||
-- a signal arrives and the process has a callback for it, it is still resumed,
|
if signal and signals[signal[1]] then
|
||||||
-- though, to call the callback.
|
xpcall(signals[signal[1]], onSignalError, select(2, table.unpack(signal)))
|
||||||
-- 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)
|
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
sleep = g.math.max(0, sleep - g.os.clock())
|
signal = {coroutine.yield(result)}
|
||||||
end
|
end
|
||||||
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)
|
local result, message = pcall(main)
|
||||||
-- if not result then error(message) end end)
|
--if not result then
|
||||||
|
print(result, message)
|
||||||
if not result then
|
--end
|
||||||
print(message)
|
|
||||||
end
|
|
||||||
return result, message
|
return result, message
|
@ -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
|
|
@ -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)
|
|
@ -31,20 +31,4 @@ public @interface Callback {
|
|||||||
* The name under which the method will be available in Lua.
|
* The name under which the method will be available in Lua.
|
||||||
*/
|
*/
|
||||||
String name();
|
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;
|
|
||||||
}
|
}
|
@ -8,13 +8,11 @@ class Computer(val owner: AnyRef) extends IInternalComputerContext {
|
|||||||
|
|
||||||
def start() = false
|
def start() = false
|
||||||
|
|
||||||
|
def stop() {}
|
||||||
|
|
||||||
def update() {}
|
def update() {}
|
||||||
|
|
||||||
def lock() {}
|
def signal(name: String, args: Any*) {}
|
||||||
|
|
||||||
def unlock() {}
|
|
||||||
|
|
||||||
def signal(pid: Int, name: String, args: Any*) {}
|
|
||||||
|
|
||||||
def readFromNBT(nbt: NBTTagCompound) {}
|
def readFromNBT(nbt: NBTTagCompound) {}
|
||||||
|
|
||||||
|
@ -59,18 +59,13 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
|
|||||||
override def createTileEntity(world: World, metadata: Int) = new TileEntityComputer(world.isRemote)
|
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) {
|
override def breakBlock(world: World, x: Int, y: Int, z: Int, `side?`: Int, metadata: Int) = {
|
||||||
if (!world.isRemote) {
|
world.getBlockTileEntity(x, y, z).asInstanceOf[TileEntityComputer].turnOff()
|
||||||
val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3
|
super.breakBlock(world, x, y, z, `side?`, metadata)
|
||||||
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)
|
|
||||||
|
|
||||||
override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer,
|
override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer,
|
||||||
side: Int, hitX: Float, hitY: Float, hitZ: Float) = {
|
side: Int, hitX: Float, hitY: Float, hitZ: Float) = {
|
||||||
@ -85,6 +80,20 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) {
|
|||||||
true
|
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) =
|
def rotation(world: IBlockAccess, x: Int, y: Int, z: Int) =
|
||||||
// Renderer(down, up, north, south, west, east) -> Facing(south, west, north, east) inverted.
|
// 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))
|
Array(0, 0, 0, 2, 3, 1)(world.getBlockMetadata(x, y, z))
|
||||||
|
@ -10,12 +10,10 @@ trait IInternalComputerContext extends IComputerContext {
|
|||||||
|
|
||||||
def start(): Boolean
|
def start(): Boolean
|
||||||
|
|
||||||
|
def stop(): Unit
|
||||||
|
|
||||||
def update()
|
def update()
|
||||||
|
|
||||||
def lock()
|
|
||||||
|
|
||||||
def unlock()
|
|
||||||
|
|
||||||
def readFromNBT(nbt: NBTTagCompound)
|
def readFromNBT(nbt: NBTTagCompound)
|
||||||
|
|
||||||
def writeToNBT(nbt: NBTTagCompound)
|
def writeToNBT(nbt: NBTTagCompound)
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
package li.cil.oc.common.tileentity
|
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.nbt.NBTTagCompound
|
||||||
import net.minecraft.tileentity.TileEntity
|
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 {
|
class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnvironment {
|
||||||
def this() = this(false)
|
def this() = this(false)
|
||||||
|
MinecraftForge.EVENT_BUS.register(this)
|
||||||
|
|
||||||
|
private val hasChanged = new AtomicBoolean()
|
||||||
|
|
||||||
// ----------------------------------------------------------------------- //
|
// ----------------------------------------------------------------------- //
|
||||||
// General
|
// General
|
||||||
// ----------------------------------------------------------------------- //
|
// ----------------------------------------------------------------------- //
|
||||||
@ -16,17 +26,49 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv
|
|||||||
|
|
||||||
def turnOn() = computer.start()
|
def turnOn() = computer.start()
|
||||||
|
|
||||||
|
def turnOff() = computer.stop()
|
||||||
|
|
||||||
override def readFromNBT(nbt: NBTTagCompound) = {
|
override def readFromNBT(nbt: NBTTagCompound) = {
|
||||||
super.readFromNBT(nbt)
|
super.readFromNBT(nbt)
|
||||||
computer.readFromNBT(nbt)
|
computer.readFromNBT(nbt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override def writeToNBT(nbt: NBTTagCompound) = {
|
override def writeToNBT(nbt: NBTTagCompound) = {
|
||||||
|
println("SAVING")
|
||||||
super.writeToNBT(nbt)
|
super.writeToNBT(nbt)
|
||||||
computer.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 world = worldObj
|
||||||
|
|
||||||
|
def markAsChanged() = hasChanged.set(true)
|
||||||
}
|
}
|
@ -1,12 +1,11 @@
|
|||||||
package li.cil.oc.server.computer
|
package li.cil.oc.server.computer
|
||||||
|
|
||||||
import java.util.concurrent._
|
import java.util.concurrent._
|
||||||
import java.util.concurrent.ThreadFactory
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
|
|
||||||
import scala.Array.canBuildFrom
|
import scala.Array.canBuildFrom
|
||||||
import scala.collection.JavaConversions._
|
import scala.collection.JavaConversions._
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
import com.naef.jnlua._
|
import com.naef.jnlua._
|
||||||
|
|
||||||
@ -19,24 +18,6 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
|||||||
// General
|
// 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
|
* 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
|
* 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
|
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 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
|
* 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)
|
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
|
* The time (world time) when the computer was started. This is used for our
|
||||||
* a synchronized API call. In that case it acquires this lock and waits for
|
* custom implementation of os.clock(), which returns the amount of the time
|
||||||
* the server thread. The server thread will try to acquire the lock after
|
* the computer has been running.
|
||||||
* notifying the state thread, to make sure the call was complete before
|
|
||||||
* resuming.
|
|
||||||
*/
|
*/
|
||||||
private val driverLock = new ReentrantLock()
|
private var timeStarted = 0L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last time (system time) the update function was called by the server
|
* 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
|
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
|
* 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.
|
* 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
|
// State
|
||||||
// ----------------------------------------------------------------------- //
|
// ----------------------------------------------------------------------- //
|
||||||
|
|
||||||
/** Starts asynchronous execution of this computer if it isn't running. */
|
/** Starts asynchronous execution of this computer if it isn't running. */
|
||||||
def start(): Boolean = state match {
|
def start(): Boolean = stateMonitor.synchronized(
|
||||||
case State.Stopped => {
|
state == State.Stopped && init() && {
|
||||||
if (init()) {
|
state = State.Suspended
|
||||||
state = State.Running
|
// 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)
|
future = Executor.pool.submit(this)
|
||||||
true
|
true
|
||||||
}
|
})
|
||||||
else false
|
|
||||||
}
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stops a computer asynchronously. */
|
/** Stops a computer, possibly asynchronously. */
|
||||||
def stop(): Unit = if (state != State.Stopped) {
|
def stop(): Unit = stateMonitor.synchronized {
|
||||||
signals.clear()
|
if (state != State.Stopped) {
|
||||||
signal(0, "terminate")
|
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 luaState = lua
|
||||||
|
|
||||||
def update() {
|
def update() {
|
||||||
updateMonitor.synchronized {
|
stateMonitor.synchronized(state match {
|
||||||
if (state == State.Stopped) return
|
case State.Stopped | State.Stopping => return
|
||||||
|
case State.DriverCall => {
|
||||||
// Check if executor is waiting for a lock to interact with a driver.
|
assert(lua.getTop() == 2)
|
||||||
future.synchronized {
|
assert(lua.`type`(1) == LuaType.THREAD)
|
||||||
if (state == State.Synchronizing) {
|
assert(lua.`type`(2) == LuaType.FUNCTION)
|
||||||
// Thread is waiting to perform synchronized API call, notify it.
|
lua.resume(1, 1)
|
||||||
future.notify()
|
assert(lua.getTop() == 2)
|
||||||
// Wait until the API call completed, which is when the driver lock
|
assert(lua.`type`(2) == LuaType.TABLE)
|
||||||
// becomes available again (we lock it in the executor thread before
|
state = State.DriverReturn
|
||||||
// waiting to be notified). We need an extra lock for that because the
|
future = Executor.pool.submit(this)
|
||||||
// driver will release the lock on 'future' to do so (see lock()).
|
|
||||||
driverLock.lock()
|
|
||||||
driverLock.unlock()
|
|
||||||
}
|
}
|
||||||
|
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
|
// 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).
|
// pause, and wake it up if it did pause (because the game was paused).
|
||||||
lastUpdate = System.currentTimeMillis()
|
lastUpdate = System.currentTimeMillis
|
||||||
updateMonitor.notify()
|
|
||||||
}
|
// 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 {
|
args.foreach {
|
||||||
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
|
case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit
|
||||||
case _ => throw new IllegalArgumentException()
|
case _ => throw new IllegalArgumentException()
|
||||||
}
|
}
|
||||||
if (state != State.Stopped) {
|
stateMonitor.synchronized(state match {
|
||||||
signals.offer(new Signal(pid, name, Array(args)))
|
// We don't push new signals when stopped or shutting down.
|
||||||
// TODO cancel delayed future and schedule for immediate execution
|
case State.Stopped | State.Stopping =>
|
||||||
// if (this.synchronized(!signals.isEmpty() && state == State.Stopped)) {
|
// Currently sleeping. Cancel that and start immediately.
|
||||||
// state = State.Running
|
case State.Sleeping =>
|
||||||
// Executor.pool.execute(this)
|
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() {
|
def readFromNBT(nbt: NBTTagCompound): Unit = this.synchronized {
|
||||||
driverLock.lock()
|
// Clear out what we currently have, if anything.
|
||||||
future.synchronized {
|
stateMonitor.synchronized {
|
||||||
state = State.Synchronizing
|
assert(state != State.Running) // Lock on 'this' should guarantee this.
|
||||||
future.wait()
|
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"))
|
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()
|
val memory = lua.getTotalMemory()
|
||||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||||
val kernel = nbt.getString("kernel")
|
try {
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist)
|
// Try unpersisting Lua, because that's what all of the rest depends on.
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
|
// Clear the stack (meaning the current kernel).
|
||||||
lua.pushString(kernel)
|
lua.setTop(0)
|
||||||
lua.call(2, 1)
|
|
||||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
|
||||||
lua.setTotalMemory(memory)
|
|
||||||
|
|
||||||
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")
|
val signalsTag = nbt.getTagList("signals")
|
||||||
signals.addAll((0 until signalsTag.tagCount()).
|
signals.addAll((0 until signalsTag.tagCount()).
|
||||||
map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]).
|
map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]).
|
||||||
map(signal => {
|
map(signal => {
|
||||||
val argsTag = signal.getCompoundTag("args")
|
val argsTag = signal.getCompoundTag("args")
|
||||||
val argsLength = argsTag.getInteger("length")
|
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 {
|
(0 until argsLength).map("arg" + _).map(argsTag.getTag(_)).map {
|
||||||
case tag: NBTTagByte => tag.data
|
case tag: NBTTagByte => tag.data
|
||||||
case tag: NBTTagShort => tag.data
|
case tag: NBTTagShort => tag.data
|
||||||
@ -210,40 +237,59 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
|||||||
}.toArray)
|
}.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)
|
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||||
|
lua.setTotalMemory(memory)
|
||||||
// Start running our worker thread if we don't already have one.
|
|
||||||
if (future == null) future = Executor.pool.submit(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def writeToNBT(nbt: NBTTagCompound): Unit = {
|
def writeToNBT(nbt: NBTTagCompound): Unit = this.synchronized {
|
||||||
// If we're running we wait for the executor to yield, to get the Lua state
|
stateMonitor.synchronized {
|
||||||
// into a valid, suspended state before trying to persist it.
|
assert(state != State.Running) // Lock on 'this' should guarantee this.
|
||||||
this.synchronized {
|
assert(state != State.Stopping) // Only set while executor is running.
|
||||||
|
}
|
||||||
|
|
||||||
nbt.setInteger("state", state.id)
|
nbt.setInteger("state", state.id)
|
||||||
if (state == State.Stopped) return
|
if (state == State.Stopped) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
nbt.setInteger("baseMemory", baseMemory)
|
// Unlimit memory while persisting.
|
||||||
nbt.setDouble("timeStarted", timeStarted)
|
|
||||||
|
|
||||||
// Call pluto.persist(persistTable, _G) and store the string result.
|
|
||||||
val memory = lua.getTotalMemory()
|
val memory = lua.getTotalMemory()
|
||||||
lua.setTotalMemory(Integer.MAX_VALUE)
|
lua.setTotalMemory(Integer.MAX_VALUE)
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persist)
|
try {
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
|
// Try persisting Lua, because that's what all of the rest depends on.
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
// While in a driver call we have one object on the global stack: either
|
||||||
lua.call(2, 1)
|
// the function to call the driver with, or the result of the call.
|
||||||
val kernel = lua.toString(-1)
|
if (state == State.DriverCall || state == State.DriverReturn) {
|
||||||
lua.pop(1)
|
assert(
|
||||||
nbt.setString("kernel", kernel)
|
if (state == State.DriverCall) lua.`type`(2) == LuaType.FUNCTION
|
||||||
lua.setTotalMemory(memory)
|
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()
|
val list = new NBTTagList()
|
||||||
for (s <- signals.iterator()) {
|
for (s <- signals.iterator()) {
|
||||||
val signal = new NBTTagCompound()
|
val signal = new NBTTagCompound()
|
||||||
signal.setInteger("pid", s.pid)
|
|
||||||
signal.setString("name", s.name)
|
signal.setString("name", s.name)
|
||||||
// TODO Test with NBTTagList, but supposedly it only allows entries
|
// TODO Test with NBTTagList, but supposedly it only allows entries
|
||||||
// with the same type, so I went with this for now...
|
// 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)
|
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 = {
|
def init(): Boolean = {
|
||||||
// Creates a new state with all base libraries as well as the Pluto
|
// Creates a new state with all base libraries and the persistence library
|
||||||
// library loaded into it. This means the state has much more power than
|
// loaded into it. This means the state has much more power than it
|
||||||
// it rightfully should have, so we sandbox it a bit in the following.
|
// rightfully should have, so we sandbox it a bit in the following.
|
||||||
lua = LuaStateFactory.createState()
|
lua = LuaStateFactory.createState()
|
||||||
|
|
||||||
try {
|
// If something went wrong while creating the state there's nothing else
|
||||||
// Before doing the actual sandboxing we save the Pluto library into the
|
// we can do here...
|
||||||
// registry, since it'll be removed from the globals table.
|
if (lua == null) return false
|
||||||
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)
|
|
||||||
|
|
||||||
|
try {
|
||||||
// Push a couple of functions that override original Lua API functions or
|
// 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")
|
lua.getGlobal("os")
|
||||||
|
|
||||||
// Return ingame time for os.time().
|
// Return ingame time for os.time().
|
||||||
lua.pushJavaFunction(new JavaFunction() {
|
lua.pushJavaFunction(new JavaFunction() {
|
||||||
def invoke(lua: LuaState): Int = {
|
def invoke(lua: LuaState): Int = {
|
||||||
// Minecraft starts days at 6 o'clock, so we add six hours.
|
// Minecraft starts days at 6 o'clock, so we add those six hours.
|
||||||
lua.pushNumber((owner.world.getTotalWorldTime() + 6000.0) / 1000.0)
|
lua.pushNumber(worldTime + 6000)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -301,72 +378,146 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
|||||||
// been running, instead of the native library...
|
// been running, instead of the native library...
|
||||||
lua.pushJavaFunction(new JavaFunction() {
|
lua.pushJavaFunction(new JavaFunction() {
|
||||||
def invoke(lua: LuaState): Int = {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
lua.setField(-2, "clock")
|
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.
|
// Pop the os table.
|
||||||
lua.pop(1)
|
lua.pop(1)
|
||||||
|
|
||||||
// Run the sandboxing script. This script is presumed to be under our
|
lua.getGlobal("math")
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Install all driver callbacks into the registry. This is done once in
|
// We give each Lua state it's own randomizer, since otherwise they'd
|
||||||
// the beginning so that we can take the memory the callbacks use into
|
// use the good old rand() from C. Which can be terrible, and isn't
|
||||||
// account when computing the kernel's memory use, as well as for building
|
// necessarily thread-safe.
|
||||||
// a table of permanent values used when persisting/unpersisting the state.
|
val random = new Random
|
||||||
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)
|
|
||||||
lua.pushJavaFunction(new JavaFunction() {
|
lua.pushJavaFunction(new JavaFunction() {
|
||||||
def invoke(lua: LuaState): Int = {
|
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
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
lua.call(2, 2)
|
lua.setField(-2, "randomseed")
|
||||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable)
|
|
||||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable)
|
// 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
|
// Load the basic kernel which takes care of handling signals by managing
|
||||||
// the list of active processes. Whatever functionality we can we implement
|
// the list of active processes. Whatever functionality we can we
|
||||||
// in Lua, so we also implement most of the kernel's functionality in Lua.
|
// implement in Lua, so we also implement most of the kernel's
|
||||||
// Why? Because like this it's automatically persisted for us without
|
// functionality in Lua. Why? Because like this it's automatically
|
||||||
// having to write more additional NBT stuff.
|
// persisted for us without having to write more additional NBT stuff.
|
||||||
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/kernel.lua"), "kernel", "t")
|
lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/kernel.lua"), "kernel", "t")
|
||||||
lua.newThread()
|
lua.newThread() // Leave it as the first value on the stack.
|
||||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
|
||||||
|
|
||||||
// Run the garbage collector to get rid of stuff left behind after the
|
// 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
|
// 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
|
// 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)
|
lua.gc(LuaState.GcAction.COLLECT, 0)
|
||||||
baseMemory = lua.getTotalMemory() - lua.getFreeMemory()
|
kernelMemory = lua.getTotalMemory() - lua.getFreeMemory()
|
||||||
lua.setTotalMemory(baseMemory + 128 * 1024)
|
lua.setTotalMemory(kernelMemory + 64 * 1024)
|
||||||
|
|
||||||
// Remember when we started the computer.
|
println("Kernel uses " + (kernelMemory / 1024) + "KB of memory.")
|
||||||
timeStarted = System.currentTimeMillis()
|
|
||||||
|
|
||||||
println("Kernel uses " + baseMemory + " bytes of memory.")
|
// Clear any left-over signals from a previous run.
|
||||||
|
signals.clear()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -379,145 +530,151 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
def close() {
|
def close(): Unit = stateMonitor.synchronized(
|
||||||
|
if (state != State.Stopped) {
|
||||||
|
state = State.Stopped
|
||||||
lua.setTotalMemory(Integer.MAX_VALUE);
|
lua.setTotalMemory(Integer.MAX_VALUE);
|
||||||
lua.close()
|
lua.close()
|
||||||
lua = null
|
lua = null
|
||||||
baseMemory = 0
|
kernelMemory = 0
|
||||||
timeStarted = 0
|
|
||||||
state = State.Stopped
|
|
||||||
future = null
|
|
||||||
signals.clear()
|
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 {
|
try {
|
||||||
println("start running computer")
|
|
||||||
|
|
||||||
// See if the game appears to be paused, in which case we also pause.
|
// See if the game appears to be paused, in which case we also pause.
|
||||||
if (System.currentTimeMillis() - lastUpdate > 500)
|
if (System.currentTimeMillis - lastUpdate > 500)
|
||||||
updateMonitor.synchronized {
|
pauseMonitor.synchronized(pauseMonitor.wait())
|
||||||
updateMonitor.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
println("running computer")
|
|
||||||
|
|
||||||
// This is synchronized so that we don't run a Lua state while saving or
|
// This is synchronized so that we don't run a Lua state while saving or
|
||||||
// loading the computer to or from an NBTTagCompound.
|
// loading the computer to or from an NBTTagCompound or other stuff
|
||||||
this.synchronized {
|
// corrupting our Lua state.
|
||||||
// Push the kernel coroutine onto the stack so that we can resume it.
|
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel)
|
// The kernel thread will always be at stack index one.
|
||||||
// Get a copy to check the coroutine's status after it ran.
|
assert(lua.`type`(1) == LuaType.THREAD)
|
||||||
lua.pushValue(-1)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Resume the Lua state and remember the number of results we get.
|
// Resume the Lua state and remember the number of results we get.
|
||||||
val results = state match {
|
val results = if (driverReturn) {
|
||||||
// Current coroutine was forced to yield. Resume without injecting any
|
// If we were doing a driver call, continue where we left off.
|
||||||
// signals. Any passed arguments would simply be ignored.
|
assert(lua.getTop() == 2)
|
||||||
case State.Yielding => {
|
lua.resume(1, 1)
|
||||||
println("resuming forced yield")
|
|
||||||
lua.resume(-1, 0)
|
|
||||||
}
|
}
|
||||||
|
else signals.poll() match {
|
||||||
// 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 {
|
|
||||||
// No signal, just run any non-sleeping processes.
|
// No signal, just run any non-sleeping processes.
|
||||||
case null => {
|
case null => lua.resume(1, 0)
|
||||||
println("resuming without signal")
|
|
||||||
lua.resume(-1, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Got a signal, inject it and call any handlers (if any).
|
// Got a signal, inject it and call any handlers (if any).
|
||||||
case signal => {
|
case signal => {
|
||||||
println("injecting signal")
|
|
||||||
lua.pushInteger(signal.pid)
|
|
||||||
lua.pushString(signal.name)
|
lua.pushString(signal.name)
|
||||||
signal.args.foreach {
|
signal.args.foreach {
|
||||||
case arg: Byte => lua.pushInteger(arg)
|
case arg: Byte => lua.pushInteger(arg)
|
||||||
case arg: Short => 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: Long => lua.pushNumber(arg)
|
||||||
case arg: Float => lua.pushNumber(arg)
|
case arg: Float => lua.pushNumber(arg)
|
||||||
case arg: Double => lua.pushNumber(arg)
|
case arg: Double => lua.pushNumber(arg)
|
||||||
case arg: String => lua.pushString(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.
|
// Only queue for next execution step if the kernel is still alive.
|
||||||
if (lua.status(-(results + 1)) != 0) {
|
if (lua.status(1) == LuaState.YIELD) {
|
||||||
// See what we have. The convention is that if the first result is a
|
// Lua state yielded normally, see what we have.
|
||||||
// string with the value "timeout" the currently running coroutines was
|
stateMonitor.synchronized {
|
||||||
// forced to yield by the execution limit (i.e. the yield comes from the
|
if (state == State.Stopping) {
|
||||||
// debug hook we installed as seen in the sandbox.lua script). Otherwise
|
// Someone called stop() in the meantime.
|
||||||
// it's a normal yield, and we get the time to wait before we should try
|
close()
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
else {
|
else if (results == 1 && lua.isNumber(2)) {
|
||||||
// Lua state yielded normally, see how long we should wait before
|
// We got a number. This tells us how long we should wait before
|
||||||
// resuming the state again.
|
// resuming the state again.
|
||||||
val sleep = (lua.toNumber(-1) * 1000).toLong
|
val sleep = (lua.toNumber(2) * 1000).toLong
|
||||||
state = State.Running
|
lua.pop(results)
|
||||||
|
state = State.Sleeping
|
||||||
future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS)
|
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)
|
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 {
|
catch {
|
||||||
// The kernel should never throw. If it does, the computer crashed
|
// The kernel should never throw. If it does, the computer crashed
|
||||||
// hard, so we just close the state.
|
// hard, so we just close the state.
|
||||||
// TODO Print something to an in-game screen, a la kernel panic.
|
// TODO Print something to an in-game screen, a la kernel panic.
|
||||||
case ex: LuaRuntimeException => ex.printLuaStackTrace()
|
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())
|
// Top-level catch-anything, because otherwise those exceptions get
|
||||||
|
// gobbled up by the executor unless we call the future's get().
|
||||||
// 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 {
|
|
||||||
case t: Throwable => t.printStackTrace()
|
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. */
|
/** Signals are messages sent to the Lua state from Java asynchronously. */
|
||||||
private class Signal(val pid: Int, val name: String, val args: Array[Any]) {
|
private class Signal(val name: String, val args: Array[Any])
|
||||||
}
|
|
||||||
|
|
||||||
/** Possible states of the computer, and in particular its executor. */
|
/** Possible states of the computer, and in particular its executor. */
|
||||||
private object State extends Enumeration {
|
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")
|
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. */
|
/** The computer is up and running, executing Lua code. */
|
||||||
val Running = Value("Running")
|
val Running = Value("Running")
|
||||||
|
|
||||||
/** The computer is yielding because of its execution limit. */
|
/** The computer is currently shutting down (waiting for executor). */
|
||||||
val Yielding = Value("Yielding")
|
val Stopping = Value("Stopping")
|
||||||
|
|
||||||
/** The computer executor is waiting for the server thread. */
|
/** The computer executor is waiting for a driver call to be made. */
|
||||||
val Synchronizing = Value("Synchronizing")
|
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. */
|
/** 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 = {
|
def newThread(r: Runnable): Thread = {
|
||||||
val name = "OpenComputers-" + threadNumber.getAndIncrement()
|
val name = "OpenComputers-" + threadNumber.getAndIncrement()
|
||||||
val thread = new Thread(group, r, name, 0)
|
val thread = new Thread(group, r, name)
|
||||||
if (thread.isDaemon())
|
if (!thread.isDaemon())
|
||||||
thread.setDaemon(false)
|
thread.setDaemon(true)
|
||||||
if (thread.getPriority() != Thread.MIN_PRIORITY)
|
if (thread.getPriority() != Thread.MIN_PRIORITY)
|
||||||
thread.setPriority(Thread.MIN_PRIORITY)
|
thread.setPriority(Thread.MIN_PRIORITY)
|
||||||
return thread
|
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"
|
|
||||||
}
|
|
||||||
|
@ -2,8 +2,6 @@ package li.cil.oc.server.computer
|
|||||||
|
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
import scala.Array.canBuildFrom
|
|
||||||
|
|
||||||
import com.naef.jnlua.JavaFunction
|
import com.naef.jnlua.JavaFunction
|
||||||
import com.naef.jnlua.LuaState
|
import com.naef.jnlua.LuaState
|
||||||
|
|
||||||
@ -18,115 +16,49 @@ private[oc] class Driver(val driver: IDriver) {
|
|||||||
if (api == null || api.isEmpty()) return
|
if (api == null || api.isEmpty()) return
|
||||||
val lua = context.luaState
|
val lua = context.luaState
|
||||||
|
|
||||||
// Get or create registry entry holding API tables.
|
// Get or create table holding API tables.
|
||||||
lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
lua.getGlobal("drivers") // ... drivers?
|
||||||
if (lua.isNil(-1)) {
|
assert(!lua.isNil(-1)) // ... drivers
|
||||||
lua.pop(1)
|
|
||||||
lua.newTable()
|
|
||||||
lua.pushValue(-1)
|
|
||||||
lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create API table.
|
// Get or create API table.
|
||||||
lua.getField(-1, api)
|
lua.getField(-1, api) // ... drivers api?
|
||||||
if (lua.isNil(-1)) {
|
if (lua.isNil(-1)) { // ... drivers nil
|
||||||
lua.pop(1)
|
lua.pop(1) // ... drivers
|
||||||
lua.newTable()
|
lua.newTable() // ... drivers api
|
||||||
lua.pushValue(-1)
|
lua.pushValue(-1) // ... drivers api api
|
||||||
lua.setField(-3, api)
|
lua.setField(-3, api) // ... drivers api
|
||||||
}
|
} // ... drivers api
|
||||||
|
|
||||||
for (method <- driver.getClass().getMethods()) {
|
for (method <- driver.getClass().getMethods())
|
||||||
val annotation = method.getAnnotation(classOf[Callback])
|
method.getAnnotation(classOf[Callback]) match {
|
||||||
if (annotation != null) {
|
case null => Unit // No annotation.
|
||||||
|
case annotation => {
|
||||||
val name = annotation.name
|
val name = annotation.name
|
||||||
lua.getField(-1, name)
|
lua.getField(-1, name) // ... drivers api func?
|
||||||
if (lua.isNil(-1)) {
|
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.
|
// Entry already exists, skip it.
|
||||||
lua.pop(1)
|
lua.pop(1) // ... drivers api
|
||||||
// TODO Log warning properly via a logger.
|
// TODO Log warning properly via a logger.
|
||||||
println("WARNING: Duplicate API entry, ignoring: " + api + "." + name)
|
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.
|
private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
|
||||||
lua.pop(2)
|
def invoke(state: LuaState): Int = {
|
||||||
}
|
return 0
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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 class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction {
|
||||||
private val classOfBoolean = classOf[Boolean]
|
private val classOfBoolean = classOf[Boolean]
|
||||||
private val classOfByte = classOf[Byte]
|
private val classOfByte = classOf[Byte]
|
||||||
@ -187,12 +119,5 @@ private[oc] class Driver(val driver: IDriver) {
|
|||||||
method.invoke(driver, args)
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -5,5 +5,5 @@ import com.naef.jnlua.LuaState
|
|||||||
import net.minecraft.nbt.NBTTagCompound
|
import net.minecraft.nbt.NBTTagCompound
|
||||||
|
|
||||||
trait IComputerContext {
|
trait IComputerContext {
|
||||||
def signal(pid: Int, name: String, args: Any*)
|
def signal(name: String, args: Any*)
|
||||||
}
|
}
|
@ -8,4 +8,7 @@ import net.minecraft.world.World
|
|||||||
*/
|
*/
|
||||||
trait IComputerEnvironment {
|
trait IComputerEnvironment {
|
||||||
def world: World
|
def world: World
|
||||||
|
|
||||||
|
/** Called when the computer state changed, so it should be saved again. */
|
||||||
|
def markAsChanged(): Unit
|
||||||
}
|
}
|
@ -88,7 +88,29 @@ private[computer] object LuaStateFactory {
|
|||||||
val state = new LuaState(Integer.MAX_VALUE)
|
val state = new LuaState(Integer.MAX_VALUE)
|
||||||
try {
|
try {
|
||||||
// Load all libraries.
|
// 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 {
|
catch {
|
||||||
case ex: Throwable => {
|
case ex: Throwable => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user