diff --git a/assets/opencomputers/lib/amd64/windows/jnlua52.dll b/assets/opencomputers/lib/amd64/windows/jnlua52.dll index a317b8382..385752b83 100644 Binary files a/assets/opencomputers/lib/amd64/windows/jnlua52.dll and b/assets/opencomputers/lib/amd64/windows/jnlua52.dll differ diff --git a/assets/opencomputers/lib/x86/windows/jnlua52.dll b/assets/opencomputers/lib/x86/windows/jnlua52.dll index bd99de21a..0239e815b 100644 Binary files a/assets/opencomputers/lib/x86/windows/jnlua52.dll and b/assets/opencomputers/lib/x86/windows/jnlua52.dll differ diff --git a/assets/opencomputers/lua/boot.lua b/assets/opencomputers/lua/boot.lua new file mode 100644 index 000000000..8c0feefa1 --- /dev/null +++ b/assets/opencomputers/lua/boot.lua @@ -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 \ No newline at end of file diff --git a/assets/opencomputers/lua/kernel.lua b/assets/opencomputers/lua/kernel.lua index c742775d4..8e515b63a 100644 --- a/assets/opencomputers/lua/kernel.lua +++ b/assets/opencomputers/lua/kernel.lua @@ -1,155 +1,339 @@ ---[[ - Basic OS functionality, such as launching new programs and loading drivers. +--[[ Basic functionality, drives userland and enforces timeouts. - This is called as the main coroutine by the computer. If this throws, the - computer crashes. If this returns, the computer is considered powered off. -]] + This is called as the main coroutine by the computer. If this throws, the + computer crashes. If this returns, the computer is considered powered off. +--]] + +--[[ Will be adjusted by the kernel when running, represents how long we can + continue running without yielding. Used in the debug hook that enforces + this timeout by throwing an error if it's exceeded. ]] +local deadline = 0 + +--[[ The hook installed for process coroutines enforcing the timeout. ]] +local function timeoutHook() + local now = os.clock() + if now > deadline then + error({timeout=debug.traceback(2)}, 0) + end +end + +--[[ Set up the global environment we make available to userland programs. ]] +local function buildSandbox() + local sandbox = { + -- Top level values. The selection of kept methods rougly follows the list + -- as available on the Lua wiki here: http://lua-users.org/wiki/SandBoxes + assert = assert, + error = error, + pcall = pcall, + xpcall = xpcall, + + ipairs = ipairs, + next = next, + pairs = pairs, + + rawequal = rawequal, + rawget = rawget, + rawlen = rawlen, + rawset = rawset, + + select = select, + type = type, + tonumber = tonumber, + tostring = tostring, + + -- We don't care what users do with metatables. The only raised concern was + -- about breaking an environment, and we don't care about that. + getmetatable = getmetatable, + setmetatable = setmetatable, + + -- Custom print that actually writes to the screen buffer. + print = print, + + bit32 = { + arshift = bit32.arshift, + band = bit32.band, + bnot = bit32.bnot, + bor = bit32.bor, + btest = bit32.btest, + bxor = bit32.bxor, + extract = bit32.extract, + replace = bit32.replace, + lrotate = bit32.lrotate, + lshift = bit32.lshift, + rrotate = bit32.rrotate, + rshift = bit32.rshift + }, + + coroutine = { + create = coroutine.create, + resume = coroutine.resume, + running = coroutine.running, + status = coroutine.status, + wrap = coroutine.wrap, + yield = coroutine.yield + }, + + math = { + abs = math.abs, + acos = math.acos, + asin = math.asin, + atan = math.atan, + atan2 = math.atan2, + ceil = math.ceil, + cos = math.cos, + cosh = math.cosh, + deg = math.deg, + exp = math.exp, + floor = math.floor, + fmod = math.fmod, + frexp = math.frexp, + huge = math.huge, + ldexp = math.ldexp, + log = math.log, + max = math.max, + min = math.min, + modf = math.modf, + pi = math.pi, + pow = math.pow, + rad = math.rad, + random = math.random, + randomseed = math.randomseed, + sin = math.sin, + sinh = math.sinh, + sqrt = math.sqrt, + tan = math.tan, + tanh = math.tanh + }, + + os = { + clock = os.clock, + date = os.date, + difftime = os.difftime, + time = os.time, + freeMemory = os.freeMemory, + totalMemory = os.totalMemory + }, + + string = { + byte = string.byte, + char = string.char, + dump = string.dump, + find = string.find, + format = string.format, + gmatch = string.gmatch, + gsub = string.gsub, + len = string.len, + lower = string.lower, + match = string.match, + rep = string.rep, + reverse = string.reverse, + sub = string.sub, + upper = string.upper + }, + + table = { + concat = table.concat, + insert = table.insert, + pack = table.pack, + remove = table.remove, + sort = table.sort, + unpack = unpack, + -- Custom functions. + copy = table.copy, + asreadonly = table.asreadonly, + isreadonly = table.isreadonly + } + } + sandbox._G = sandbox + + -- Allow sandboxes to load code, but only in text form, and in the sandbox. + -- Note that we allow passing a custom environment, because if this is called + -- from inside the sandbox, env must already be in the sandbox. + function sandbox.load(code, env) + return load(code, nil, "t", env or sandbox) + end + + -- Make methods respect the read-only aspect of tables. + do + local function checkreadonly(t) + if table.isreadonly(t) then + error("trying to modify read-only table", 3) + end + end + function sandbox.rawset(t, k, v) + checkreadonly(t) + rawset(t, k, v) + end + function sandbox.table.insert(t, k, v) + checkreadonly(t) + table.insert(t, k, v) + end + function sandbox.table.remove(t, k) + checkreadonly(t) + table.remove(t, k) + end + function sandbox.table.sort(t, f) + checkreadonly(t) + table.sort(t, f) + end + end + + --[[ Error thrower available to the userland. Used to differentiate system + errors from user errors, such as timeouts (we rethrow system errors). + --]] + function sandbox.error(message, level) + level = math.max(0, level or 1) + error({message=message}, level > 0 and level + 1 or 0) + end + + local function checkResult(success, result, ...) + if success then + return success, result, ... + end + if result.timeout then + error({timeout=result.timeout .. "\n" .. debug.traceback(2)}, 0) + end + return success, result.message + end + + function sandbox.pcall(f, ...) + return checkResult(pcall(f, ...)) + end + + function sandbox.xpcall(f, msgh, ...) + function handler(msg) + return msg.message and {message=msgh(msg.message)} or msg + end + return checkResult(xpcall(f, handler, ...)) + end + + --[[ Install wrappers for coroutine management that reserves the first value + returned by yields for internal stuff. + --]] + function sandbox.coroutine.yield(...) + return coroutine.yield(nil, ...) + end + function sandbox.coroutine.resume(co, ...) + if not debug.gethook(co) then -- Don't reset counter. + debug.sethook(co, timeoutHook, "", 10000) + end + local result = {checkResult(coroutine.resume(co, ...))} + if result[1] and result[2] then + -- Internal yield, bubble up to the top and resume where we left off. + return coroutine.resume(co, coroutine.yield(result[2])) + end + -- Userland yield or error, just pass it on. + return result[1], select(3, table.unpack(result)) + end + + --[[ Suspends the computer for the specified amount of time. Note that + signal handlers will still be called if a signal arrives. + --]] + function sandbox.os.sleep(seconds) + checkType(1, seconds, "number") + local target = os.clock() + seconds + while os.clock() < target do + -- Yielding a number here will tell the host it can wait with running us + -- again for that long. Note that this is *not* a sleep! We may be resumed + -- way sooner, e.g. because of signals or a state load (after an unload). + -- That's why we put a loop around the thing. + coroutine.yield(seconds) + end + end + + return sandbox +end local function main() - --[[ - A copy the globals table to avoid user-space programs messing with us. The - actual copy is created below, because we first need to declare the table copy - function... which in turn uses this variable to avoid being tampered with. - ]] - local g = _G + --[[ Create the sandbox as a thread-local variable so it is persisted. ]] + local sandbox = buildSandbox() - -- List of all active processes. - local processes = {} - - -- The ID of the process currently running. - local currentProcess = 0 - - --[[ - Returns the process ID of the currently executing process. - ]] - function _G.os.pid() - return currentProcess - end - - -- Starts a new process using the specified callback. - function _G.os.execute(task) - local callback = task - if g.type(task) == "string" then - -- Check if we have a file system, load script and set callback. - -- TODO ... - g.setfenv(callback, g.setmetatable({}, {__index = _G})) - end - g.table.insert(processes, { - pid = #processes, - thread = g.coroutine.create(callback), - parent = currentProcess, - sleep = 0, - signals = {} - }) - end - - --[[ Stops the process currently being executed. ]] - function _G.os.exit() - g.coroutine.yield("terminate") - end - - --[[ Makes the current process sleep for the specified amount of time. ]] - function _G.os.sleep(seconds) - assert(g.type(seconds) == "number", - g.string.format("'number' expected, got '%s'", g.type(seconds))) - processes[currentProcess].sleep = g.os.clock() + seconds - while processes[currentProcess].sleep > g.os.clock() do - local signal = {g.coroutine.yield()} - if signal[1] then - processes[currentProcess][signal[1]](g.unpack(signal)) - end - end - end + --[[ List of signal handlers, by name. ]] + local signals = setmetatable({}, {__mode = "v"}) --[[ Registers or unregisters a callback for a signal. ]] - function _G.os.signal(name, callback) - assert(g.type(name) == "string" and g.type(callback) == "function", - g.string.format("'string', 'function' expected, got '%s', '%s'", - g.type(name), g.type(callback))) - processes[currentProcess][name] = callback + function sandbox.os.signal(name, callback) + checkType(1, name, "string") + checkType(2, callback, "function", "nil") + + local oldCallback = signals[name] + signals[name] = callback + return oldCallback end - -- We replace the default yield function so that be can differentiate between - -- process level yields and coroutine level yields - in case a process spawns - -- new coroutines. - --[[ - function _G.coroutine.yield(...) - while true do - local result = {g.coroutine.yield(nil, ...)} - if result[1] == "signal" then - end - end - end - - function _G.coroutine.resume(...) - while true do - local result = {g.coroutine.resume(...)} - if result[1] and result[2] == "signal" then - + --[[ Error handler for signal callbacks. ]] + local function onSignalError(msg) + if type(msg) == "table" then + if msg.timeout then + msg = "too long without yielding" else - return result[1], g.select(3, g.unpack(result)) + msg = msg.message end end + print(msg) + return msg end - ]] - -- Create the actual copy now that we have our copying function. - g = g.table.copy(g, true) + --[[ Set up the shell, which is really just a Lua interpreter. ]] + local shellThread + local function startShell(safeMode) + -- Set sandbox environment and create the shell runner. + local _ENV = sandbox + local function shell() + function test(arg) + print(string.format("%d SIGNAL! Available RAM: %d/%d", + os.time(), os.freeMemory(), os.totalMemory())) + end + os.signal("test", test) - -- Spawn the init process, which is basically a Lua interpreter. - g.os.execute(function() print("hi") os.sleep(5) end) + local i = 0 + while true do + i = i + 1 + print("ping " .. i) + os.sleep(1) + end + end + shellThread = coroutine.create(shell) + end + startShell() print("Running kernel...") - while true do - print("ping") - g.coroutine.yield(5) - end - - -- Begin running our processes. We run all processes consecutively if they are - -- currently "awake", meaning not waiting for a call to os.sleep to return. If - -- a signal arrives and the process has a callback for it, it is still resumed, - -- though, to call the callback. - -- If all processes are asleep, we yield the minimum sleep time, so that we can - -- avoid busy waiting (resuming the main coroutine over and over again). - local sleep = 0 - while #processes > 0 do - local signal = {g.coroutine.yield(sleep)} - local signalPid = signal[1] - local signalName = signal[2] - local signalArgs = g.select(3, g.unpack(signal)) - - for _, process in ipairs(processes) do - local awake = g.os.clock() >= process.sleep - local target = process.signals[signalName] and - (signalPid < 1 or signalPid == process.pid) - if awake or target then - currentProcess = process.pid - local result, cause = g.coroutine.resume(process.thread, "signal", signalName, g.unpack(signalArgs)) - if not result or g.coroutine.status(process.thread) == "dead" then - process.thread = nil - elseif cause == "terminate" then - process.thread = nil + -- Pending signal to be processed. We only either process a signal *or* run + -- the shell, to keep the chance of long blocks low (and "accidentally" going + -- over the timeout). + local signal + while coroutine.status(shellThread) ~= "dead" do + deadline = os.clock() + 5 + local result + if signal and signals[signal[1]] then + xpcall(signals[signal[1]], onSignalError, select(2, table.unpack(signal))) + else + 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 - end - end - sleep = g.math.huge - for i = #processes, 1, -1 do - if processes[i].thread == nil then - g.table.remove(processes, i) + elseif status[2].timeout then + -- Timeout, restart the shell but don't start user scripts this time. + startShell(true) else - sleep = g.math.min(processes[i].sleep, sleep) + -- Some other error, go kill ourselves. + error(result[2], 0) end end - sleep = g.math.max(0, sleep - g.os.clock()) + signal = {coroutine.yield(result)} end end --- local result, message = pcall1(function() +-- JNLua (or possibly Lua itself) sucks at propagating error messages across +-- resumes that were triggered from the native side, so we do a pcall here. local result, message = pcall(main) --- if not result then error(message) end end) - -if not result then - print(message) -end +--if not result then + print(result, message) +--end return result, message \ No newline at end of file diff --git a/assets/opencomputers/lua/persistence.lua b/assets/opencomputers/lua/persistence.lua deleted file mode 100644 index 7ddcda726..000000000 --- a/assets/opencomputers/lua/persistence.lua +++ /dev/null @@ -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 \ No newline at end of file diff --git a/assets/opencomputers/lua/sandbox.lua b/assets/opencomputers/lua/sandbox.lua deleted file mode 100644 index f57bcf8f6..000000000 --- a/assets/opencomputers/lua/sandbox.lua +++ /dev/null @@ -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) \ No newline at end of file diff --git a/li/cil/oc/api/Callback.java b/li/cil/oc/api/Callback.java index 1978f2b12..e7d537e93 100644 --- a/li/cil/oc/api/Callback.java +++ b/li/cil/oc/api/Callback.java @@ -31,20 +31,4 @@ public @interface Callback { * The name under which the method will be available in Lua. */ String name(); - - /** - * Whether this API function is called synchronized with the server thread. - * - * Each computer runs its Lua state using a separate thread, to avoid slowing - * down the game or lock it up completely. This means that when a state calls - * a driver's API function this code is run in such a worker thread that is - * not synchronized with the server thread (i.e. the main game thread). The - * driver must therefore take care to avoid threading issues. For convenience - * a driver can request that an API function is only called in sync with the - * server thread. In that case the computer's thread will stop and wait for - * the server thread to come along. The server thread will see that the - * computer thread is waiting for it and perform it's API call for it, then - * let it loose again and continue with what it was doing. - */ - boolean synchronize() default false; } \ No newline at end of file diff --git a/li/cil/oc/client/computer/Computer.scala b/li/cil/oc/client/computer/Computer.scala index be65c7973..25bd271a8 100644 --- a/li/cil/oc/client/computer/Computer.scala +++ b/li/cil/oc/client/computer/Computer.scala @@ -7,14 +7,12 @@ class Computer(val owner: AnyRef) extends IInternalComputerContext { def luaState = null def start() = false + + def stop() {} def update() {} - def lock() {} - - def unlock() {} - - def signal(pid: Int, name: String, args: Any*) {} + def signal(name: String, args: Any*) {} def readFromNBT(nbt: NBTTagCompound) {} diff --git a/li/cil/oc/common/block/BlockComputer.scala b/li/cil/oc/common/block/BlockComputer.scala index 92de705d2..9d6f06592 100644 --- a/li/cil/oc/common/block/BlockComputer.scala +++ b/li/cil/oc/common/block/BlockComputer.scala @@ -59,19 +59,14 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) { override def createTileEntity(world: World, metadata: Int) = new TileEntityComputer(world.isRemote) // ----------------------------------------------------------------------- // - // Block rotation + // Destruction / Interaction // ----------------------------------------------------------------------- // - override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) { - if (!world.isRemote) { - val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3 - setRotation(world, x, y, z, facing) - } + override def breakBlock(world: World, x: Int, y: Int, z: Int, `side?`: Int, metadata: Int) = { + world.getBlockTileEntity(x, y, z).asInstanceOf[TileEntityComputer].turnOff() + super.breakBlock(world, x, y, z, `side?`, metadata) } - - override def getValidRotations(world: World, x: Int, y: Int, z: Int) = - Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST) - + override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer, side: Int, hitX: Float, hitY: Float, hitZ: Float) = { if (player.isSneaking()) @@ -85,6 +80,20 @@ class BlockComputer extends Block(Config.blockComputerId, Material.iron) { true } + // ----------------------------------------------------------------------- // + // Block rotation + // ----------------------------------------------------------------------- // + + override def onBlockPlacedBy(world: World, x: Int, y: Int, z: Int, entity: EntityLivingBase, itemStack: ItemStack) { + if (!world.isRemote) { + val facing = MathHelper.floor_double(entity.rotationYaw * 4 / 360 + 0.5) & 3 + setRotation(world, x, y, z, facing) + } + } + + override def getValidRotations(world: World, x: Int, y: Int, z: Int) = + Array(ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.NORTH, ForgeDirection.EAST) + def rotation(world: IBlockAccess, x: Int, y: Int, z: Int) = // Renderer(down, up, north, south, west, east) -> Facing(south, west, north, east) inverted. Array(0, 0, 0, 2, 3, 1)(world.getBlockMetadata(x, y, z)) diff --git a/li/cil/oc/common/computer/IInternalComputerContext.scala b/li/cil/oc/common/computer/IInternalComputerContext.scala index f817c3e3b..717acf556 100644 --- a/li/cil/oc/common/computer/IInternalComputerContext.scala +++ b/li/cil/oc/common/computer/IInternalComputerContext.scala @@ -10,12 +10,10 @@ trait IInternalComputerContext extends IComputerContext { def start(): Boolean + def stop(): Unit + def update() - def lock() - - def unlock() - def readFromNBT(nbt: NBTTagCompound) def writeToNBT(nbt: NBTTagCompound) diff --git a/li/cil/oc/common/tileentity/TileEntityComputer.scala b/li/cil/oc/common/tileentity/TileEntityComputer.scala index 959ddbf17..87db03d52 100644 --- a/li/cil/oc/common/tileentity/TileEntityComputer.scala +++ b/li/cil/oc/common/tileentity/TileEntityComputer.scala @@ -1,11 +1,21 @@ package li.cil.oc.common.tileentity -import li.cil.oc.server.computer.IComputerEnvironment +import java.util.concurrent.atomic.AtomicBoolean + +import li.cil.oc.server.computer.IComputerEnvironment import net.minecraft.nbt.NBTTagCompound import net.minecraft.tileentity.TileEntity +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.event.ForgeSubscribe +import net.minecraftforge.event.world.ChunkEvent +import net.minecraftforge.event.world.WorldEvent class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnvironment { def this() = this(false) + MinecraftForge.EVENT_BUS.register(this) + + private val hasChanged = new AtomicBoolean() + // ----------------------------------------------------------------------- // // General // ----------------------------------------------------------------------- // @@ -16,17 +26,49 @@ class TileEntityComputer(isClient: Boolean) extends TileEntity with IComputerEnv def turnOn() = computer.start() + def turnOff() = computer.stop() + override def readFromNBT(nbt: NBTTagCompound) = { super.readFromNBT(nbt) computer.readFromNBT(nbt) } override def writeToNBT(nbt: NBTTagCompound) = { + println("SAVING") super.writeToNBT(nbt) computer.writeToNBT(nbt) } - override def updateEntity() = computer.update() + override def updateEntity() = { + computer.update() + if (hasChanged.get()) + worldObj.updateTileEntityChunkAndDoNothing( + this.xCoord, this.yCoord, this.zCoord, this) + } + + // ----------------------------------------------------------------------- // + // Event Bus + // ----------------------------------------------------------------------- // + + @ForgeSubscribe + def onChunkUnload(e: ChunkEvent.Unload) = { + println("CHUNK UNLOADING") + MinecraftForge.EVENT_BUS.unregister(this) + computer.stop() + } + + @ForgeSubscribe + def onWorldUnload(e: WorldEvent.Unload) = { + println("WORLD UNLOADING") + MinecraftForge.EVENT_BUS.unregister(this) + computer.stop() + } + + // ----------------------------------------------------------------------- // + // IComputerEnvironment + // ----------------------------------------------------------------------- // def world = worldObj + + def markAsChanged() = hasChanged.set(true) } \ No newline at end of file diff --git a/li/cil/oc/server/computer/Computer.scala b/li/cil/oc/server/computer/Computer.scala index 0df75fd64..a60fac058 100644 --- a/li/cil/oc/server/computer/Computer.scala +++ b/li/cil/oc/server/computer/Computer.scala @@ -1,12 +1,11 @@ package li.cil.oc.server.computer import java.util.concurrent._ -import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.ReentrantLock import scala.Array.canBuildFrom import scala.collection.JavaConversions._ +import scala.util.Random import com.naef.jnlua._ @@ -19,24 +18,6 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext // General // ----------------------------------------------------------------------- // - /** The internal Lua state. Only set while the computer is running. */ - private var lua: LuaState = null - - /** - * The base memory consumption of the kernel. Used to permit a fixed base - * memory for user-space programs even if the amount of memory the kernel - * uses changes over time (i.e. with future releases of the mod). This is set - * when starting up the computer. - */ - private var baseMemory = 0 - - /** - * The time when the computer was started. This is used for our custom - * implementation of os.clock(), which returns the amount of the time the - * computer has been running. - */ - private var timeStarted = 0.0 - /** * The current execution state of the computer. This is used to track how to * resume the computers main thread, if at all, and whether to accept new @@ -44,6 +25,17 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext */ private var state = State.Stopped + /** The internal Lua state. Only set while the computer is running. */ + private var lua: LuaState = null + + /** + * The base memory consumption of the kernel. Used to permit a fixed base + * memory for userland even if the amount of memory the kernel uses changes + * over time (i.e. with future releases of the mod). This is set when + * starting up the computer. + */ + private var kernelMemory = 0 + /** * The queue of signals the Lua state should process. Signals are queued from * the Java side and processed one by one in the Lua VM. They are the only @@ -52,22 +44,14 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext */ private val signals = new LinkedBlockingQueue[Signal](256) - /** - * This is used to keep track of the current executor of the Lua state, for - * example to wait for the computer to finish running a task. This is used to - * cancel scheduled execution when a new signal arrives and to wait for the - * computer to shut down. - */ - private var future: Future[_] = null + // ----------------------------------------------------------------------- // /** - * This lock is used by the thread executing the Lua state when it performs - * a synchronized API call. In that case it acquires this lock and waits for - * the server thread. The server thread will try to acquire the lock after - * notifying the state thread, to make sure the call was complete before - * resuming. + * The time (world time) when the computer was started. This is used for our + * custom implementation of os.clock(), which returns the amount of the time + * the computer has been running. */ - private val driverLock = new ReentrantLock() + private var timeStarted = 0L /** * The last time (system time) the update function was called by the server @@ -76,42 +60,67 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext */ private var lastUpdate = 0L + /** + * The current world time. This is used for our custom implementation of + * os.time(). This is updated by the server thread and read by the computer + * thread, to avoid computer threads directly accessing the world state. + */ + private var worldTime = 0L + + // ----------------------------------------------------------------------- // + + /** + * This is used to keep track of the current executor of the Lua state, for + * example to wait for the computer to finish running a task. + */ + private var future: Future[_] = null + /** * The object our executor thread waits on if the last update has been a * while, and the update function calls notify on each time it is run. */ - private val updateMonitor = new Object() + private val pauseMonitor = new Object() + + /** This is used to synchronize access to the state field. */ + private val stateMonitor = new Object() // ----------------------------------------------------------------------- // // State // ----------------------------------------------------------------------- // /** Starts asynchronous execution of this computer if it isn't running. */ - def start(): Boolean = state match { - case State.Stopped => { - if (init()) { - state = State.Running - future = Executor.pool.submit(this) - true + def start(): Boolean = stateMonitor.synchronized( + state == State.Stopped && init() && { + state = State.Suspended + // Inject a dummy signal so that real one don't get swallowed. This way + // we can just ignore the parameters the first time the kernel is run. + signal("dummy") + future = Executor.pool.submit(this) + true + }) + + /** Stops a computer, possibly asynchronously. */ + def stop(): Unit = stateMonitor.synchronized { + if (state != State.Stopped) { + if (state != State.Running) { + // If the computer is not currently running we can simply close it, + // and cancel any pending future - which may already be running and + // waiting for the stateMonitor, so we do a hard abort. + if (future != null) { + future.cancel(true) + } + close() + } + else { + // Otherwise we enter an intermediate state to ensure the executor + // truly stopped, before switching back to stopped to allow starting + // the computer again. The executor will check for this state and + // call close. + state = State.Stopping + // Make sure the thread isn't waiting for an update. + pauseMonitor.synchronized(pauseMonitor.notify()) } - else false } - case _ => false - } - - /** Stops a computer asynchronously. */ - def stop(): Unit = if (state != State.Stopped) { - signals.clear() - signal(0, "terminate") - } - - /** Stops a computer synchronously. */ - def stopAndWait(): Unit = { - stop() - // Get a local copy to avoid having to synchronize it between the null - // check and the actual wait. - val future = this.future - if (future != null) future.get() } // ----------------------------------------------------------------------- // @@ -121,84 +130,102 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext def luaState = lua def update() { - updateMonitor.synchronized { - if (state == State.Stopped) return - - // Check if executor is waiting for a lock to interact with a driver. - future.synchronized { - if (state == State.Synchronizing) { - // Thread is waiting to perform synchronized API call, notify it. - future.notify() - // Wait until the API call completed, which is when the driver lock - // becomes available again (we lock it in the executor thread before - // waiting to be notified). We need an extra lock for that because the - // driver will release the lock on 'future' to do so (see lock()). - driverLock.lock() - driverLock.unlock() - } + stateMonitor.synchronized(state match { + case State.Stopped | State.Stopping => return + case State.DriverCall => { + assert(lua.getTop() == 2) + assert(lua.`type`(1) == LuaType.THREAD) + assert(lua.`type`(2) == LuaType.FUNCTION) + lua.resume(1, 1) + assert(lua.getTop() == 2) + assert(lua.`type`(2) == LuaType.TABLE) + state = State.DriverReturn + future = Executor.pool.submit(this) } + case _ => /* nothing special to do */ + }) - // Update last time run to let our executor thread know it doesn't have to - // pause, and wake it up if it did pause (because the game was paused). - lastUpdate = System.currentTimeMillis() - updateMonitor.notify() + // Remember when we started the computer for os.clock(). We do this in the + // update because only then can we be sure the world is available. + if (timeStarted == 0) + timeStarted = owner.world.getWorldInfo().getWorldTotalTime() + + // Update world time for computer threads. + worldTime = owner.world.getWorldInfo().getWorldTotalTime() + + if (worldTime % 40 == 0) { + signal("test", "ha!") } + + // Update last time run to let our executor thread know it doesn't have to + // pause, and wake it up if it did pause (because the game was paused). + lastUpdate = System.currentTimeMillis + + // Tell the executor thread it may continue if it's waiting. + pauseMonitor.synchronized(pauseMonitor.notify()) } - def signal(pid: Int, name: String, args: Any*) = { + def signal(name: String, args: Any*) = { args.foreach { case _: Byte | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit case _ => throw new IllegalArgumentException() } - if (state != State.Stopped) { - signals.offer(new Signal(pid, name, Array(args))) - // TODO cancel delayed future and schedule for immediate execution - // if (this.synchronized(!signals.isEmpty() && state == State.Stopped)) { - // state = State.Running - // Executor.pool.execute(this) - // } + stateMonitor.synchronized(state match { + // We don't push new signals when stopped or shutting down. + case State.Stopped | State.Stopping => + // Currently sleeping. Cancel that and start immediately. + case State.Sleeping => + future.cancel(true) + state = State.Suspended + signals.offer(new Signal(name, args.toArray)) + future = Executor.pool.submit(this) + // Running or in driver call or only a short yield, just push the signal. + case _ => + signals.offer(new Signal(name, args.toArray)) + }) + } + + def readFromNBT(nbt: NBTTagCompound): Unit = this.synchronized { + // Clear out what we currently have, if anything. + stateMonitor.synchronized { + assert(state != State.Running) // Lock on 'this' should guarantee this. + stop() } - } - def lock() { - driverLock.lock() - future.synchronized { - state = State.Synchronizing - future.wait() - } - } + state = State(nbt.getInteger("state")) - def unlock() { - driverLock.unlock() - } + if (state != State.Stopped && init()) { + // Unlimit memory use while unpersisting. + val memory = lua.getTotalMemory() + lua.setTotalMemory(Integer.MAX_VALUE) + try { + // Try unpersisting Lua, because that's what all of the rest depends on. + // Clear the stack (meaning the current kernel). + lua.setTop(0) - def readFromNBT(nbt: NBTTagCompound): Unit = { - // If we're running we wait for the executor to yield, to get the Lua state - // into a valid, suspended state before trying to unpersist into it. - this.synchronized { - state = State(nbt.getInteger("state")) - if (state != State.Stopped && (lua != null || init())) { - baseMemory = nbt.getInteger("baseMemory") - timeStarted = nbt.getDouble("timeStarted") + if (!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.") + } + } - val memory = lua.getTotalMemory() - lua.setTotalMemory(Integer.MAX_VALUE) - val kernel = nbt.getString("kernel") - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist) - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable) - lua.pushString(kernel) - lua.call(2, 1) - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel) - lua.setTotalMemory(memory) - - signals.clear() + assert(signals.size() == 0) val signalsTag = nbt.getTagList("signals") signals.addAll((0 until signalsTag.tagCount()). map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]). map(signal => { val argsTag = signal.getCompoundTag("args") val argsLength = argsTag.getInteger("length") - new Signal(signal.getInteger("pid"), signal.getString("name"), + new Signal(signal.getString("name"), (0 until argsLength).map("arg" + _).map(argsTag.getTag(_)).map { case tag: NBTTagByte => tag.data case tag: NBTTagShort => tag.data @@ -210,40 +237,59 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext }.toArray) })) - lua.gc(LuaState.GcAction.COLLECT, 0) + timeStarted = nbt.getLong("timeStarted") - // Start running our worker thread if we don't already have one. - if (future == null) future = Executor.pool.submit(this) + // 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.setTotalMemory(memory) } } } - def writeToNBT(nbt: NBTTagCompound): Unit = { - // If we're running we wait for the executor to yield, to get the Lua state - // into a valid, suspended state before trying to persist it. - this.synchronized { - nbt.setInteger("state", state.id) - if (state == State.Stopped) return + def writeToNBT(nbt: NBTTagCompound): Unit = this.synchronized { + stateMonitor.synchronized { + assert(state != State.Running) // Lock on 'this' should guarantee this. + assert(state != State.Stopping) // Only set while executor is running. + } - nbt.setInteger("baseMemory", baseMemory) - nbt.setDouble("timeStarted", timeStarted) + nbt.setInteger("state", state.id) + if (state == State.Stopped) { + return + } - // Call pluto.persist(persistTable, _G) and store the string result. - val memory = lua.getTotalMemory() - lua.setTotalMemory(Integer.MAX_VALUE) - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persist) - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable) - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel) - lua.call(2, 1) - val kernel = lua.toString(-1) - lua.pop(1) - nbt.setString("kernel", kernel) - lua.setTotalMemory(memory) + // Unlimit memory while persisting. + val memory = lua.getTotalMemory() + lua.setTotalMemory(Integer.MAX_VALUE) + try { + // Try persisting Lua, because that's what all of the rest depends on. + // While in a driver call we have one object on the global stack: either + // the function to call the driver with, or the result of the call. + if (state == State.DriverCall || state == State.DriverReturn) { + assert( + if (state == State.DriverCall) lua.`type`(2) == LuaType.FUNCTION + else lua.`type`(2) == LuaType.TABLE) + nbt.setByteArray("stack", persist()) + } + // Save the kernel state (which is always at stack index one). + assert(lua.`type`(1) == LuaType.THREAD) + nbt.setByteArray("kernel", persist()) val list = new NBTTagList() for (s <- signals.iterator()) { val signal = new NBTTagCompound() - signal.setInteger("pid", s.pid) signal.setString("name", s.name) // TODO Test with NBTTagList, but supposedly it only allows entries // with the same type, so I went with this for now... @@ -263,35 +309,66 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext } nbt.setTag("signals", list) + nbt.setLong("timeStarted", timeStarted) + } + catch { + case t: Throwable => { + t.printStackTrace() + nbt.setInteger("state", State.Stopped.id) + } + } + finally { + // Clean up some after we're done and limit memory again. lua.gc(LuaState.GcAction.COLLECT, 0) + lua.setTotalMemory(memory) } } + private def persist(): Array[Byte] = { + lua.getGlobal("persist") // ... obj persist? + if (lua.`type`(-1) == LuaType.FUNCTION) { // ... obj persist + lua.pushValue(-2) // ... obj persist obj + lua.call(1, 1) // ... obj str? + if (lua.`type`(-1) == LuaType.STRING) { // ... obj str + val result = lua.toByteArray(-1) + lua.pop(1) // ... obj + return result + } // ... obj :( + } // ... obj :( + lua.pop(1) // ... obj + return Array[Byte]() + } + + private def unpersist(value: Array[Byte]): Boolean = { + lua.getGlobal("unpersist") // ... unpersist? + if (lua.`type`(-1) == LuaType.FUNCTION) { // ... unpersist + lua.pushByteArray(value) // ... unpersist str + lua.call(1, 1) // ... obj + return true + } // ... :( + return false + } + def init(): Boolean = { - // Creates a new state with all base libraries as well as the Pluto - // library loaded into it. This means the state has much more power than - // it rightfully should have, so we sandbox it a bit in the following. + // Creates a new state with all base libraries and the persistence library + // loaded into it. This means the state has much more power than it + // rightfully should have, so we sandbox it a bit in the following. lua = LuaStateFactory.createState() - try { - // Before doing the actual sandboxing we save the Pluto library into the - // registry, since it'll be removed from the globals table. - lua.getGlobal("eris") - lua.getField(-1, "persist") - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persist) - lua.getField(-1, "unpersist") - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersist) - lua.pop(1) + // If something went wrong while creating the state there's nothing else + // we can do here... + if (lua == null) return false + try { // Push a couple of functions that override original Lua API functions or - // that add new functionality to it. + // that add new functionality to it.1) lua.getGlobal("os") // Return ingame time for os.time(). lua.pushJavaFunction(new JavaFunction() { def invoke(lua: LuaState): Int = { - // Minecraft starts days at 6 o'clock, so we add six hours. - lua.pushNumber((owner.world.getTotalWorldTime() + 6000.0) / 1000.0) + // Minecraft starts days at 6 o'clock, so we add those six hours. + lua.pushNumber(worldTime + 6000) return 1 } }) @@ -301,72 +378,146 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext // been running, instead of the native library... lua.pushJavaFunction(new JavaFunction() { def invoke(lua: LuaState): Int = { - lua.pushNumber(owner.world.getTotalWorldTime() - timeStarted) + // World time is in ticks, and each second has 20 ticks. Since we + // want os.clock() to return real seconds, though, we'll divide it + // accordingly. + lua.pushNumber((owner.world.getTotalWorldTime() - timeStarted) / 20.0) return 1 } }) lua.setField(-2, "clock") - // TODO Other overrides? + // Custom os.difftime(). For most Lua implementations this would be the + // same anyway, but just to be on the safe side. + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + val t2 = lua.checkNumber(1) + val t1 = lua.checkNumber(2) + lua.pushNumber(t2 - t1) + return 1 + } + }) + lua.setField(-2, "difftime") + + // Allow the system to read how much memory it uses and has available. + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + lua.pushInteger(lua.getTotalMemory() - kernelMemory) + return 1 + } + }) + lua.setField(-2, "totalMemory") + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + lua.pushInteger(lua.getFreeMemory()) + return 1 + } + }) + lua.setField(-2, "freeMemory") // Pop the os table. lua.pop(1) - // Run the sandboxing script. This script is presumed to be under our - // control. We do the sandboxing in Lua because it'd be a pain to write - // using only stack operations... - lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/sandbox.lua"), "sandbox", "t") - lua.call(0, 0) + lua.getGlobal("math") - // Install all driver callbacks into the registry. This is done once in - // the beginning so that we can take the memory the callbacks use into - // account when computing the kernel's memory use, as well as for building - // a table of permanent values used when persisting/unpersisting the state. - Drivers.injectInto(this) - - // Run the script that builds the tables with permanent values. These - // tables must contain all java callbacks (i.e. C functions, since they - // are wrapped on the native side using a C function, of course). They - // are used when persisting/unpersisting the state so that Pluto knows - // which values it doesn't have to serialize (since it cannot persist C - // functions). We store the two tables in the registry. - // TODO These tables may change after loading a game, for example due to - // a new mod being installed or an old one being removed. In that case, - // previously existing values will "suddenly" become nil. We may want to - // consider detecting such changes and rebooting computers in that case. - lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/persistence.lua"), "persistence", "t") - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis) + // We give each Lua state it's own randomizer, since otherwise they'd + // use the good old rand() from C. Which can be terrible, and isn't + // necessarily thread-safe. + val random = new Random lua.pushJavaFunction(new JavaFunction() { def invoke(lua: LuaState): Int = { - println(lua.toString(1)) + lua.getTop() match { + case 0 => lua.pushNumber(random.nextDouble) + case 1 => { + val u = lua.checkInteger(1) + lua.checkArg(1, 1 < u, "interval is empty") + lua.pushInteger(1 + random.nextInt(u)) + } + case 2 => { + val l = lua.checkInteger(1) + val u = lua.checkInteger(2) + lua.checkArg(1, l < u, "interval is empty") + lua.pushInteger(l + random.nextInt(u - l)) + } + case _ => throw new IllegalArgumentException("wrong number of arguments") + } + return 1 + } + }) + lua.setField(-2, "random") + + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + val seed = lua.checkInteger(1) + random.setSeed(seed) return 0 } }) - lua.call(2, 2) - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.unpersistTable) - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.persistTable) + lua.setField(-2, "randomseed") + + // Pop the math table. + lua.pop(1) + + // Until we get to ingame screens we log to Java's stdout. + lua.pushJavaFunction(new JavaFunction() { + def invoke(lua: LuaState): Int = { + for (i <- 1 to lua.getTop()) { + lua.`type`(i) match { + case LuaType.NIL => print("nil") + case LuaType.BOOLEAN => print(lua.toBoolean(i)) + case LuaType.NUMBER => print(lua.toNumber(i)) + case LuaType.STRING => print(lua.toString(i)) + case LuaType.TABLE => print("table") + case LuaType.FUNCTION => print("function") + case LuaType.THREAD => print("thread") + case LuaType.LIGHTUSERDATA | LuaType.USERDATA => print("userdata") + } + } + return 0 + } + }) + lua.setGlobal("print") + + // TODO Other overrides? + + // Install all driver callbacks into the state. This is done once in + // the beginning so that we can take the memory the callbacks use into + // account when computing the kernel's memory use, as well as for + // building a table of permanent values used when persisting/unpersisting + // the state. + lua.newTable() + lua.setGlobal("drivers") + Drivers.injectInto(this) + + // Run the boot script. This creates the global sandbox variable that is + // used as the environment for any processes the kernel spawns, adds a + // couple of library functions and sets up the permanent value tables as + // well as making the functions used for persisting/unpersisting + // available as globals. + lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/boot.lua"), "boot", "t") + lua.call(0, 0) // Load the basic kernel which takes care of handling signals by managing - // the list of active processes. Whatever functionality we can we implement - // in Lua, so we also implement most of the kernel's functionality in Lua. - // Why? Because like this it's automatically persisted for us without - // having to write more additional NBT stuff. + // the list of active processes. Whatever functionality we can we + // implement in Lua, so we also implement most of the kernel's + // functionality in Lua. Why? Because like this it's automatically + // persisted for us without having to write more additional NBT stuff. lua.load(classOf[Computer].getResourceAsStream("/assets/opencomputers/lua/kernel.lua"), "kernel", "t") - lua.newThread() - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel) + lua.newThread() // Leave it as the first value on the stack. // Run the garbage collector to get rid of stuff left behind after the // initialization phase to get a good estimate of the base memory usage // the kernel has. We remember that size to grant user-space programs a - // fixed base amount of memory. + // fixed base amount of memory, regardless of the memory need of the + // underlying system (which may change across releases). lua.gc(LuaState.GcAction.COLLECT, 0) - baseMemory = lua.getTotalMemory() - lua.getFreeMemory() - lua.setTotalMemory(baseMemory + 128 * 1024) + kernelMemory = lua.getTotalMemory() - lua.getFreeMemory() + lua.setTotalMemory(kernelMemory + 64 * 1024) - // Remember when we started the computer. - timeStarted = System.currentTimeMillis() + println("Kernel uses " + (kernelMemory / 1024) + "KB of memory.") - println("Kernel uses " + baseMemory + " bytes of memory.") + // Clear any left-over signals from a previous run. + signals.clear() return true } @@ -379,145 +530,151 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext return false } - def close() { - lua.setTotalMemory(Integer.MAX_VALUE); - lua.close() - lua = null - baseMemory = 0 - timeStarted = 0 - state = State.Stopped - future = null - signals.clear() - } + def close(): Unit = stateMonitor.synchronized( + if (state != State.Stopped) { + state = State.Stopped + lua.setTotalMemory(Integer.MAX_VALUE); + lua.close() + lua = null + kernelMemory = 0 + signals.clear() + timeStarted = 0 + future = null + }) + + // This is a really high level lock that we only use for saving and loading. + def run(): Unit = this.synchronized { + println(" > executor enter") + + val driverReturn = State.DriverReturn == stateMonitor.synchronized { + val oldState = state + state = State.Running + oldState + } - def run() { try { - println("start running computer") - // See if the game appears to be paused, in which case we also pause. - if (System.currentTimeMillis() - lastUpdate > 500) - updateMonitor.synchronized { - updateMonitor.wait() - } - - println("running computer") + if (System.currentTimeMillis - lastUpdate > 500) + pauseMonitor.synchronized(pauseMonitor.wait()) // This is synchronized so that we don't run a Lua state while saving or - // loading the computer to or from an NBTTagCompound. - this.synchronized { - // Push the kernel coroutine onto the stack so that we can resume it. - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.kernel) - // Get a copy to check the coroutine's status after it ran. - lua.pushValue(-1) + // loading the computer to or from an NBTTagCompound or other stuff + // corrupting our Lua state. - try { - // Resume the Lua state and remember the number of results we get. - val results = state match { - // Current coroutine was forced to yield. Resume without injecting any - // signals. Any passed arguments would simply be ignored. - case State.Yielding => { - println("resuming forced yield") - lua.resume(-1, 0) - } + // The kernel thread will always be at stack index one. + assert(lua.`type`(1) == LuaType.THREAD) - // 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. - case null => { - println("resuming without signal") - lua.resume(-1, 0) - } + // Resume the Lua state and remember the number of results we get. + val results = if (driverReturn) { + // If we were doing a driver call, continue where we left off. + assert(lua.getTop() == 2) + lua.resume(1, 1) + } + else signals.poll() match { + // No signal, just run any non-sleeping processes. + case null => lua.resume(1, 0) - // Got a signal, inject it and call any handlers (if any). - case signal => { - println("injecting signal") - lua.pushInteger(signal.pid) - lua.pushString(signal.name) - signal.args.foreach { - case arg: Byte => lua.pushInteger(arg) - case arg: Short => lua.pushInteger(arg) - case arg: Integer => lua.pushInteger(arg) - case arg: Long => lua.pushNumber(arg) - case arg: Float => lua.pushNumber(arg) - case arg: Double => lua.pushNumber(arg) - case arg: String => lua.pushString(arg) - } - lua.resume(-1, 2 + signal.args.length) - } - } - } + // Got a signal, inject it and call any handlers (if any). + case signal => { + lua.pushString(signal.name) + signal.args.foreach { + case arg: Byte => lua.pushInteger(arg) + case arg: Short => lua.pushInteger(arg) + case arg: Int => lua.pushInteger(arg) + case arg: Long => lua.pushNumber(arg) + case arg: Float => lua.pushNumber(arg) + case arg: Double => lua.pushNumber(arg) + case arg: String => lua.pushString(arg) } - - println("lua yielded") - - // Only queue for next execution step if the kernel is still alive. - if (lua.status(-(results + 1)) != 0) { - // See what we have. The convention is that if the first result is a - // string with the value "timeout" the currently running coroutines was - // forced to yield by the execution limit (i.e. the yield comes from the - // debug hook we installed as seen in the sandbox.lua script). Otherwise - // it's a normal yield, and we get the time to wait before we should try - // to execute the state again in seconds. - if (lua.isString(-results) && "timeout".equals(lua.toString(-results))) { - // Forced yield due to long execution time. Remember this for the next - // time we run, so we don't try to insert a signal which would get - // ignored. - state = State.Yielding - future = Executor.pool.submit(this) - } - else { - // Lua state yielded normally, see how long we should wait before - // resuming the state again. - val sleep = (lua.toNumber(-1) * 1000).toLong - state = State.Running - future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS) - } - } - lua.pop(results) - } - catch { - // The kernel should never throw. If it does, the computer crashed - // hard, so we just close the state. - // TODO Print something to an in-game screen, a la kernel panic. - case ex: LuaRuntimeException => ex.printLuaStackTrace() - case ex: Throwable => ex.printStackTrace() - } - println("free memory: " + lua.getFreeMemory()) - - // If the kernel is no longer running the computer has stopped. - lua.status(-1) match { - case LuaState.YIELD => lua.pop(1) - case _ => updateMonitor.synchronized(close()) + lua.resume(1, 1 + signal.args.length) } } - println("end running computer") + // State has inevitably changed, mark as changed to save again. + owner.markAsChanged() + + // Only queue for next execution step if the kernel is still alive. + if (lua.status(1) == LuaState.YIELD) { + // Lua state yielded normally, see what we have. + stateMonitor.synchronized { + if (state == State.Stopping) { + // Someone called stop() in the meantime. + close() + } + else if (results == 1 && lua.isNumber(2)) { + // We got a number. This tells us how long we should wait before + // resuming the state again. + val sleep = (lua.toNumber(2) * 1000).toLong + lua.pop(results) + state = State.Sleeping + future = Executor.pool.schedule(this, sleep, TimeUnit.MILLISECONDS) + } + else if (results == 1 && lua.isFunction(2)) { + // If we get one function it's a wrapper for a driver call. + state = State.DriverCall + future = null + } + else { + // Something else, just pop the results and try again. + lua.pop(results) + state = State.Suspended + future = Executor.pool.submit(this) + } + } + + println(" < executor leave") + + // Avoid getting to the closing part after the exception handling. + return + } } catch { + // The kernel should never throw. If it does, the computer crashed + // hard, so we just close the state. + // TODO Print something to an in-game screen, a la kernel panic. + case ex: LuaRuntimeException => ex.printLuaStackTrace() + case 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() + } + // Top-level catch-anything, because otherwise those exceptions get + // gobbled up by the executor unless we call the future's get(). case t: Throwable => t.printStackTrace() } + + // If we come here there was an error or we stopped, kill off the state. + close() + + println(" < executor leave") } - /** Signals are messages sent to the Lua state's processes from Java. */ - private class Signal(val pid: Int, val name: String, val args: Array[Any]) { - } + /** Signals are messages sent to the Lua state from Java asynchronously. */ + private class Signal(val name: String, val args: Array[Any]) /** Possible states of the computer, and in particular its executor. */ private object State extends Enumeration { - /** Self explanatory: the computer is not running right now. */ + /** The computer is not running right now and there is no Lua state. */ val Stopped = Value("Stopped") + /** The computer is running but yielded for a moment. */ + val Suspended = Value("Suspended") + + /** The computer is running but yielding for a longer amount of time. */ + val Sleeping = Value("Sleeping") + /** The computer is up and running, executing Lua code. */ val Running = Value("Running") - /** The computer is yielding because of its execution limit. */ - val Yielding = Value("Yielding") + /** The computer is currently shutting down (waiting for executor). */ + val Stopping = Value("Stopping") - /** The computer executor is waiting for the server thread. */ - val Synchronizing = Value("Synchronizing") + /** The computer executor is waiting for a driver call to be made. */ + val DriverCall = Value("DriverCall") + + /** The computer should resume with the result of a driver call. */ + val DriverReturn = Value("DriverReturn") } /** Singleton for requesting executors that run our Lua states. */ @@ -532,9 +689,9 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext def newThread(r: Runnable): Thread = { val name = "OpenComputers-" + threadNumber.getAndIncrement() - val thread = new Thread(group, r, name, 0) - if (thread.isDaemon()) - thread.setDaemon(false) + val thread = new Thread(group, r, name) + if (!thread.isDaemon()) + thread.setDaemon(true) if (thread.getPriority() != Thread.MIN_PRIORITY) thread.setPriority(Thread.MIN_PRIORITY) return thread @@ -542,13 +699,3 @@ class Computer(val owner: IComputerEnvironment) extends IInternalComputerContext }) } } - -/** Names of entries in the registries of the Lua states of computers. */ -private[computer] object ComputerRegistry { - val kernel = "oc_kernel" - val driverApis = "oc_apis" - val persist = "oc_persist" - val unpersist = "oc_unpersist" - val unpersistTable = "oc_unpersistTable" - val persistTable = "oc_persistTable" -} diff --git a/li/cil/oc/server/computer/Driver.scala b/li/cil/oc/server/computer/Driver.scala index 1155e5919..1c94fa814 100644 --- a/li/cil/oc/server/computer/Driver.scala +++ b/li/cil/oc/server/computer/Driver.scala @@ -2,8 +2,6 @@ package li.cil.oc.server.computer import java.lang.reflect.Method -import scala.Array.canBuildFrom - import com.naef.jnlua.JavaFunction import com.naef.jnlua.LuaState @@ -18,115 +16,49 @@ private[oc] class Driver(val driver: IDriver) { if (api == null || api.isEmpty()) return val lua = context.luaState - // Get or create registry entry holding API tables. - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis) - if (lua.isNil(-1)) { - lua.pop(1) - lua.newTable() - lua.pushValue(-1) - lua.setField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis) - } + // Get or create table holding API tables. + lua.getGlobal("drivers") // ... drivers? + assert(!lua.isNil(-1)) // ... drivers // Get or create API table. - lua.getField(-1, api) - if (lua.isNil(-1)) { - lua.pop(1) - lua.newTable() - lua.pushValue(-1) - lua.setField(-3, api) - } + lua.getField(-1, api) // ... drivers api? + if (lua.isNil(-1)) { // ... drivers nil + lua.pop(1) // ... drivers + lua.newTable() // ... drivers api + lua.pushValue(-1) // ... drivers api api + lua.setField(-3, api) // ... drivers api + } // ... drivers api - for (method <- driver.getClass().getMethods()) { - val annotation = method.getAnnotation(classOf[Callback]) - if (annotation != null) { - val name = annotation.name - lua.getField(-1, name) - if (lua.isNil(-1)) { - // Entry already exists, skip it. - lua.pop(1) - // TODO Log warning properly via a logger. - println("WARNING: Duplicate API entry, ignoring: " + api + "." + name) - } - else { - // No such entry yet. Pop the nil and build our callback wrapper. - lua.pop(1) - if (annotation.synchronize) { - lua.pushJavaFunction(new SynchronizedMethodWrapper(context, method)) + for (method <- driver.getClass().getMethods()) + method.getAnnotation(classOf[Callback]) match { + case null => Unit // No annotation. + case annotation => { + val name = annotation.name + lua.getField(-1, name) // ... drivers api func? + if (lua.isNil(-1)) { // ... drivers api nil + // No such entry yet. + lua.pop(1) // ... drivers api + lua.pushJavaFunction(new MethodWrapper(context, method)) // ... drivers api func + lua.setField(-2, name) // ... drivers api } - else { - lua.pushJavaFunction(new MethodWrapper(context, method)) + else { // ... drivers api func + // Entry already exists, skip it. + lua.pop(1) // ... drivers api + // TODO Log warning properly via a logger. + println("WARNING: Duplicate API entry, ignoring: " + api + "." + name) } - lua.setField(-2, name) } - } - } - - // Pop the API table and the table holding all APIs. - lua.pop(2) + } // ... drivers api + lua.pop(2) // ... } - /** - * This installs the driver on the computer, providing an API to interact - * with the device. - * - * This copies an existing API table from the registry and executes any - * initialization code provided by the driver. - */ - def install(context: IInternalComputerContext) { - copyAPI(context) - - // Do we have custom initialization code? - val code = driver.apiCode - if (code != null && !code.isEmpty()) { - val lua = context.luaState - lua.load(code, driver.componentName) - // TODO Set environment so that variables not explicitly added to globals - // table won't accidentally clutter it. - lua.call(0, 0) + private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction { + def invoke(state: LuaState): Int = { + return 0 } } - private def copyAPI(context: IInternalComputerContext) { - // Check if the component actually provides an API. - val api = driver.apiName - if (api == null && api.isEmpty()) return - - // Get the Lua state and check if the API table already exists. - val lua = context.luaState - - lua.getField(LuaState.REGISTRYINDEX, ComputerRegistry.driverApis) - if (lua.isNil(-1)) { - // We don't have any APIs at all. - lua.pop(1) - return - } - - lua.getField(-1, api) - if (lua.isNil(-1)) { - // No such API. Which is kind of weird, but hey. - lua.pop(2) - return - } - - // OK, we have our registry table. Create a new table to copy into. - val registryTable = lua.getTop() - lua.newTable() - val globalTable = lua.getTop() - - // Copy all keys (which are the API functions). - lua.pushNil() - while (lua.next(registryTable)) { - val key = lua.toString(-2) - lua.setField(globalTable, key) - } - - // Push our globals table into the global name space. - lua.setGlobal(api) - - // Pop the registry API table and registry table holding all API tables. - lua.pop(2) - } - + /* private class MethodWrapper(val context: IInternalComputerContext, val method: Method) extends JavaFunction { private val classOfBoolean = classOf[Boolean] private val classOfByte = classOf[Byte] @@ -187,12 +119,5 @@ private[oc] class Driver(val driver: IDriver) { method.invoke(driver, args) } } - - private class SynchronizedMethodWrapper(context: IInternalComputerContext, method: Method) extends MethodWrapper(context, method) { - override def call(args: AnyRef*) = { - context.lock() - try super.call(args) - finally context.unlock() - } - } +*/ } \ No newline at end of file diff --git a/li/cil/oc/server/computer/IComputerContext.scala b/li/cil/oc/server/computer/IComputerContext.scala index c022887c4..1db5cea9d 100644 --- a/li/cil/oc/server/computer/IComputerContext.scala +++ b/li/cil/oc/server/computer/IComputerContext.scala @@ -5,5 +5,5 @@ import com.naef.jnlua.LuaState import net.minecraft.nbt.NBTTagCompound trait IComputerContext { - def signal(pid: Int, name: String, args: Any*) + def signal(name: String, args: Any*) } \ No newline at end of file diff --git a/li/cil/oc/server/computer/IComputerEnvironment.scala b/li/cil/oc/server/computer/IComputerEnvironment.scala index 4907efccd..26e0380f8 100644 --- a/li/cil/oc/server/computer/IComputerEnvironment.scala +++ b/li/cil/oc/server/computer/IComputerEnvironment.scala @@ -8,4 +8,7 @@ import net.minecraft.world.World */ trait IComputerEnvironment { def world: World + + /** Called when the computer state changed, so it should be saved again. */ + def markAsChanged(): Unit } \ No newline at end of file diff --git a/li/cil/oc/server/computer/LuaStateFactory.scala b/li/cil/oc/server/computer/LuaStateFactory.scala index 99b0659ea..5acdfd6dd 100644 --- a/li/cil/oc/server/computer/LuaStateFactory.scala +++ b/li/cil/oc/server/computer/LuaStateFactory.scala @@ -88,7 +88,29 @@ private[computer] object LuaStateFactory { val state = new LuaState(Integer.MAX_VALUE) try { // Load all libraries. - state.openLibs() + state.openLib(LuaState.Library.BASE) + state.openLib(LuaState.Library.BIT32) + state.openLib(LuaState.Library.COROUTINE) + state.openLib(LuaState.Library.DEBUG) + state.openLib(LuaState.Library.ERIS) + state.openLib(LuaState.Library.MATH) + state.openLib(LuaState.Library.STRING) + state.openLib(LuaState.Library.TABLE) + state.pop(8) + + // Prepare table for os stuff. + state.newTable() + state.setGlobal("os") + + // Remove some other functions we don't need and are dangerous. + state.pushNil() + state.setGlobal("dofile") + state.pushNil() + state.setGlobal("loadfile") + state.pushNil() + state.setGlobal("module") + state.pushNil() + state.setGlobal("require") } catch { case ex: Throwable => {