diff --git a/li/cil/oc/server/component/Machine.scala b/li/cil/oc/server/component/Machine.scala index f0ecfc714..d4decc8b5 100644 --- a/li/cil/oc/server/component/Machine.scala +++ b/li/cil/oc/server/component/Machine.scala @@ -1,16 +1,14 @@ package li.cil.oc.server.component -import com.naef.jnlua._ -import java.io.{FileNotFoundException, IOException} import java.util.logging.Level import li.cil.oc.api import li.cil.oc.api.network._ import li.cil.oc.common.tileentity import li.cil.oc.server import li.cil.oc.server.PacketSender -import li.cil.oc.util.ExtendedLuaState.extendLuaState +import li.cil.oc.server.component.machine.{ExecutionResult, LuaArchitecture} import li.cil.oc.util.ExtendedNBT._ -import li.cil.oc.util.{ThreadPoolFactory, GameTimeFormatter, LuaStateFactory} +import li.cil.oc.util.ThreadPoolFactory import li.cil.oc.{OpenComputers, Settings} import net.minecraft.entity.player.EntityPlayer import net.minecraft.nbt._ @@ -19,7 +17,6 @@ import net.minecraft.server.integrated.IntegratedServer import net.minecraft.world.World import scala.Array.canBuildFrom import scala.Some -import scala.collection.convert.WrapAsScala._ import scala.collection.mutable class Machine(val owner: Machine.Owner) extends ManagedComponent with Context with Runnable { @@ -36,13 +33,11 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi fromMemory(Settings.get.tmpSize * 1024), "tmpfs")) } else None - private val state = mutable.Stack(Machine.State.Stopped) + private val architecture = new LuaArchitecture(this) - private var lua: LuaState = null + private[component] val state = mutable.Stack(Machine.State.Stopped) - private var kernelMemory = 0 - - private val components = mutable.Map.empty[String, String] + private[component] val components = mutable.Map.empty[String, String] private val addedComponents = mutable.Set.empty[Component] @@ -52,17 +47,15 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi private val callCounts = mutable.Map.empty[String, mutable.Map[String, Int]] - private val ramScale = if (LuaStateFactory.is64Bit) Settings.get.ramScaleFor64Bit else 1.0 - // ----------------------------------------------------------------------- // - private var timeStarted = 0L // Game-world time [ms] for os.uptime(). + private[component] var timeStarted = 0L // Game-world time [ms] for os.uptime(). - private var worldTime = 0L // Game-world time for os.time(). + private[component] var worldTime = 0L // Game-world time for os.time(). - private var cpuTime = 0L // Pseudo-real-world time [ns] for os.clock(). + private[component] var cpuTime = 0L // Pseudo-real-world time [ns] for os.clock(). - private var cpuStart = 0L // Pseudo-real-world time [ns] for os.clock(). + private[component] var cpuStart = 0L // Pseudo-real-world time [ns] for os.clock(). private var remainIdle = 0 // Ticks left to sleep before resuming. @@ -70,19 +63,11 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi private var usersChanged = false // Send updated users list to clients? - private var message: Option[String] = None // For error messages. + private[component] var message: Option[String] = None // For error messages. // ----------------------------------------------------------------------- // - def recomputeMemory() = Option(lua) match { - case Some(l) => - l.setTotalMemory(Int.MaxValue) - l.gc(LuaState.GcAction.COLLECT, 0) - if (kernelMemory > 0) { - l.setTotalMemory(kernelMemory + math.ceil(owner.installedMemory * ramScale).toInt) - } - case _ => - } + def recomputeMemory() = architecture.recomputeMemory() def lastError = message @@ -202,6 +187,50 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi } }) + private[component] def popSignal(): Option[Machine.Signal] = signals.synchronized(if (signals.isEmpty) None else Some(signals.dequeue())) + + private[component] def invoke(address: String, method: String, args: Seq[AnyRef]) = + Option(node.network.node(address)) match { + case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => + val direct = component.isDirect(method) + if (direct) callCounts.synchronized { + val limit = component.limit(method) + val counts = callCounts.getOrElseUpdate(component.address, mutable.Map.empty[String, Int]) + val count = counts.getOrElseUpdate(method, 0) + if (count >= limit) { + throw new Machine.LimitReachedException() + } + counts(method) += 1 + } + component.invoke(method, this, args: _*) + case _ => throw new Exception("no such component") + } + + private[component] def addUser(name: String) { + if (_users.size >= Settings.get.maxUsers) + throw new Exception("too many users") + + if (_users.contains(name)) + throw new Exception("user exists") + if (name.length > Settings.get.maxUsernameLength) + throw new Exception("username too long") + if (!MinecraftServer.getServer.getConfigurationManager.getAllUsernames.contains(name)) + throw new Exception("player must be online") + + _users.synchronized { + _users += name + usersChanged = true + } + } + + private[component] def removeUser(name: String) = _users.synchronized { + val success = _users.remove(name) + if (success) { + usersChanged = true + } + success + } + // ----------------------------------------------------------------------- // @LuaCallback("start") @@ -302,10 +331,6 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi } // Perform a synchronized call (message sending). case Machine.State.SynchronizedCall => - // These three asserts are all guaranteed by run(). - assert(lua.getTop == 2) - assert(lua.isThread(1)) - assert(lua.isFunction(2)) // Clear direct call limits again, just to be on the safe side... // Theoretically it'd be possible for the executor to do some direct // calls between the clear and the state check, which could in turn @@ -315,11 +340,7 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi // were performed from our executor thread. switchTo(Machine.State.Running) try { - // Synchronized call protocol requires the called function to return - // a table, which holds the results of the call, to be passed back - // to the coroutine.yield() that triggered the call. - lua.call(0, 1) - lua.checkType(2, LuaType.TABLE) + architecture.runSynchronized() // Check if the callback called pause() or stop(). state.top match { case Machine.State.Running => @@ -333,15 +354,10 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi case _ => throw new AssertionError() } } catch { - case _: LuaMemoryAllocationException => - // This can happen if we run out of memory while converting a Java - // exception to a string (which we have to do to avoid keeping - // userdata on the stack, which cannot be persisted). - crash("not enough memory") case e: java.lang.Error if e.getMessage == "not enough memory" => crash("not enough memory") case e: Throwable => - OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e) + OpenComputers.log.log(Level.WARNING, "Faulty architecture implementation for synchronized calls.", e) crash("protocol error") } case _ => // Nothing special to do, just avoid match errors. @@ -430,7 +446,7 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi components.synchronized(components += component.address -> component.name) // Skip the signal if we're not initialized yet, since we'd generate a // duplicate in the startup script otherwise. - if (kernelMemory > 0) { + if (architecture.isInitialized) { signal("component_added", component.address, component.name) } } @@ -468,28 +484,7 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi nbt.getTagList("users").foreach[NBTTagString](u => _users += u.data) if (state.size > 0 && state.top != Machine.State.Stopped && init()) { - // Unlimit memory use while unpersisting. - lua.setTotalMemory(Integer.MAX_VALUE) - - try { - // Try unpersisting Lua, because that's what all of the rest depends - // on. First, clear the stack, meaning the current kernel. - lua.setTop(0) - - unpersist(nbt.getByteArray("kernel")) - if (!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 IllegalArgumentException("Invalid kernel.") - } - if (state.contains(Machine.State.SynchronizedCall) || state.contains(Machine.State.SynchronizedReturn)) { - unpersist(nbt.getByteArray("stack")) - if (!(if (state.contains(Machine.State.SynchronizedCall)) lua.isFunction(2) else lua.isTable(2))) { - // Same as with the above, should not really happen normally, but - // could for the same reasons. - throw new IllegalArgumentException("Invalid stack.") - } - } + architecture.load(nbt) components ++= nbt.getTagList("components").iterator[NBTTagCompound].map(c => c.getString("address") -> c.getString("name")) @@ -519,7 +514,6 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi rom.foreach(rom => rom.load(nbt.getCompoundTag("rom"))) tmp.foreach(tmp => tmp.load(nbt.getCompoundTag("tmp"))) - kernelMemory = (nbt.getInteger("kernelMemory") * ramScale).toInt timeStarted = nbt.getLong("timeStarted") cpuTime = nbt.getLong("cpuTime") remainingPause = nbt.getInteger("remainingPause") @@ -527,16 +521,8 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi message = Some(nbt.getString("message")) } - // Limit memory again. - recomputeMemory() - // Delay execution for a second to allow the world around us to settle. pause(Settings.get.startupDelay) - } catch { - case e: LuaRuntimeException => - OpenComputers.log.warning("Could not unpersist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) - state.push(Machine.State.Stopping) - } } else close() // Clean up in case we got a weird state stack. } @@ -556,130 +542,61 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi nbt.setNewTagList("users", _users) if (state.top != Machine.State.Stopped) { - // Unlimit memory while persisting. - lua.setTotalMemory(Integer.MAX_VALUE) + architecture.save(nbt) - try { - // Try persisting Lua, because that's what all of the rest depends on. - // Save the kernel state (which is always at stack index one). - assert(lua.isThread(1)) - nbt.setByteArray("kernel", persist(1)) - // 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.contains(Machine.State.SynchronizedCall) || state.contains(Machine.State.SynchronizedReturn)) { - assert(if (state.contains(Machine.State.SynchronizedCall)) lua.isFunction(2) else lua.isTable(2)) - nbt.setByteArray("stack", persist(2)) - } - - val componentsNbt = new NBTTagList() - for ((address, name) <- components) { - val componentNbt = new NBTTagCompound() - componentNbt.setString("address", address) - componentNbt.setString("name", name) - componentsNbt.appendTag(componentNbt) - } - nbt.setTag("components", componentsNbt) - - val signalsNbt = new NBTTagList() - for (s <- signals.iterator) { - val signalNbt = new NBTTagCompound() - signalNbt.setString("name", s.name) - signalNbt.setNewCompoundTag("args", args => { - args.setInteger("length", s.args.length) - s.args.zipWithIndex.foreach { - case (Unit, i) => args.setByte("arg" + i, -1) - case (arg: Boolean, i) => args.setByte("arg" + i, if (arg) 1 else 0) - case (arg: Double, i) => args.setDouble("arg" + i, arg) - case (arg: String, i) => args.setString("arg" + i, arg) - case (arg: Array[Byte], i) => args.setByteArray("arg" + i, arg) - case (arg: Map[String, String], i) => - val list = new NBTTagList() - for ((key, value) <- arg) { - list.append(key) - list.append(value) - } - args.setTag("arg" + i, list) - case (_, i) => args.setByte("arg" + i, -1) - } - }) - signalsNbt.appendTag(signalNbt) - } - nbt.setTag("signals", signalsNbt) - - rom.foreach(rom => nbt.setNewCompoundTag("rom", rom.save)) - tmp.foreach(tmp => nbt.setNewCompoundTag("tmp", tmp.save)) - - nbt.setInteger("kernelMemory", math.ceil(kernelMemory / ramScale).toInt) - nbt.setLong("timeStarted", timeStarted) - nbt.setLong("cpuTime", cpuTime) - nbt.setInteger("remainingPause", remainingPause) - message.foreach(nbt.setString("message", _)) - } catch { - case e: LuaRuntimeException => - OpenComputers.log.warning("Could not persist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) - nbt.removeTag("state") + val componentsNbt = new NBTTagList() + for ((address, name) <- components) { + val componentNbt = new NBTTagCompound() + componentNbt.setString("address", address) + componentNbt.setString("name", name) + componentsNbt.appendTag(componentNbt) } + nbt.setTag("components", componentsNbt) - // Limit memory again. - recomputeMemory() + val signalsNbt = new NBTTagList() + for (s <- signals.iterator) { + val signalNbt = new NBTTagCompound() + signalNbt.setString("name", s.name) + signalNbt.setNewCompoundTag("args", args => { + args.setInteger("length", s.args.length) + s.args.zipWithIndex.foreach { + case (Unit, i) => args.setByte("arg" + i, -1) + case (arg: Boolean, i) => args.setByte("arg" + i, if (arg) 1 else 0) + case (arg: Double, i) => args.setDouble("arg" + i, arg) + case (arg: String, i) => args.setString("arg" + i, arg) + case (arg: Array[Byte], i) => args.setByteArray("arg" + i, arg) + case (arg: Map[String, String], i) => + val list = new NBTTagList() + for ((key, value) <- arg) { + list.append(key) + list.append(value) + } + args.setTag("arg" + i, list) + case (_, i) => args.setByte("arg" + i, -1) + } + }) + signalsNbt.appendTag(signalNbt) + } + nbt.setTag("signals", signalsNbt) + + rom.foreach(rom => nbt.setNewCompoundTag("rom", rom.save)) + tmp.foreach(tmp => nbt.setNewCompoundTag("tmp", tmp.save)) + + nbt.setLong("timeStarted", timeStarted) + nbt.setLong("cpuTime", cpuTime) + nbt.setInteger("remainingPause", remainingPause) + message.foreach(nbt.setString("message", _)) } } - private def persist(index: Int): Array[Byte] = { - lua.getGlobal("eris") /* ... eris */ - lua.getField(-1, "persist") /* ... eris persist */ - if (lua.isFunction(-1)) { - lua.getField(LuaState.REGISTRYINDEX, "perms") /* ... eris persist perms */ - lua.pushValue(index) // ... eris persist perms obj - try { - lua.call(2, 1) // ... eris str? - } catch { - case e: Throwable => - lua.pop(1) - throw e - } - if (lua.isString(-1)) { - // ... eris str - val result = lua.toByteArray(-1) - lua.pop(2) // ... - return result - } // ... eris :( - } // ... eris :( - lua.pop(2) // ... - Array[Byte]() - } - - private def unpersist(value: Array[Byte]): Boolean = { - lua.getGlobal("eris") // ... eris - lua.getField(-1, "unpersist") // ... eris unpersist - if (lua.isFunction(-1)) { - lua.getField(LuaState.REGISTRYINDEX, "uperms") /* ... eris persist uperms */ - lua.pushByteArray(value) // ... eris unpersist uperms str - lua.call(2, 1) // ... eris obj - lua.insert(-2) // ... obj eris - lua.pop(1) - return true - } // ... :( - lua.pop(1) - false - } - // ----------------------------------------------------------------------- // private def init(): Boolean = { // Reset error state. message = None - // 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. - LuaStateFactory.createState() match { - case None => - lua = null - message = Some("native libraries not available") - return false - case Some(value) => lua = value - } + // Clear any left-over signals from a previous run. + signals.clear() // Connect the ROM and `/tmp` node to our owner. We're not in a network in // case we're loading, which is why we have to check it here. @@ -689,393 +606,7 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi } try { - // Push a couple of functions that override original Lua API functions or - // that add new functionality to it. - lua.getGlobal("os") - - // Custom os.clock() implementation returning the time the computer has - // been actively running, instead of the native library... - lua.pushScalaFunction(lua => { - lua.pushNumber((cpuTime + (System.nanoTime() - cpuStart)) * 10e-10) - 1 - }) - lua.setField(-2, "clock") - - // Date formatting function. - lua.pushScalaFunction(lua => { - val format = - if (lua.getTop > 0 && lua.isString(1)) lua.toString(1) - else "%d/%m/%y %H:%M:%S" - val time = - if (lua.getTop > 1 && lua.isNumber(2)) lua.toNumber(2) * 1000 / 60 / 60 - else worldTime + 6000 - - val dt = GameTimeFormatter.parse(time) - def fmt(format: String) { - if (format == "*t") { - lua.newTable(0, 8) - lua.pushInteger(dt.year) - lua.setField(-2, "year") - lua.pushInteger(dt.month) - lua.setField(-2, "month") - lua.pushInteger(dt.day) - lua.setField(-2, "day") - lua.pushInteger(dt.hour) - lua.setField(-2, "hour") - lua.pushInteger(dt.minute) - lua.setField(-2, "min") - lua.pushInteger(dt.second) - lua.setField(-2, "sec") - lua.pushInteger(dt.weekDay) - lua.setField(-2, "wday") - lua.pushInteger(dt.yearDay) - lua.setField(-2, "yday") - } - else { - lua.pushString(GameTimeFormatter.format(format, dt)) - } - } - - // Just ignore the allowed leading '!', Minecraft has no time zones... - if (format.startsWith("!")) - fmt(format.substring(1)) - else - fmt(format) - 1 - }) - lua.setField(-2, "date") - - // Return ingame time for os.time(). - lua.pushScalaFunction(lua => { - // Game time is in ticks, so that each day has 24000 ticks, meaning - // one hour is game time divided by one thousand. Also, Minecraft - // starts days at 6 o'clock, so we add those six hours. Thus: - // timestamp = (time + 6000) * 60[kh] * 60[km] / 1000[s] - lua.pushNumber((worldTime + 6000) * 60 * 60 / 1000) - 1 - }) - lua.setField(-2, "time") - - // Pop the os table. - lua.pop(1) - - // Computer API, stuff that kinda belongs to os, but we don't want to - // clutter it. - lua.newTable() - - // Allow getting the real world time for timeouts. - lua.pushScalaFunction(lua => { - lua.pushNumber(System.currentTimeMillis() / 1000.0) - 1 - }) - lua.setField(-2, "realTime") - - // The time the computer has been running, as opposed to the CPU time. - lua.pushScalaFunction(lua => { - // World time is in ticks, and each second has 20 ticks. Since we - // want uptime() to return real seconds, though, we'll divide it - // accordingly. - lua.pushNumber((worldTime - timeStarted) / 20.0) - 1 - }) - lua.setField(-2, "uptime") - - // Allow the computer to figure out its own id in the component network. - lua.pushScalaFunction(lua => { - Option(node.address) match { - case None => lua.pushNil() - case Some(address) => lua.pushString(address) - } - 1 - }) - lua.setField(-2, "address") - - // Are we a robot? (No this is not a CAPTCHA.) - lua.pushScalaFunction(lua => { - lua.pushBoolean(isRobot) - 1 - }) - lua.setField(-2, "isRobot") - - lua.pushScalaFunction(lua => { - // This is *very* unlikely, but still: avoid this getting larger than - // what we report as the total memory. - lua.pushInteger(((lua.getFreeMemory min (lua.getTotalMemory - kernelMemory)) / ramScale).toInt) - 1 - }) - lua.setField(-2, "freeMemory") - - // Allow the system to read how much memory it uses and has available. - lua.pushScalaFunction(lua => { - lua.pushInteger(((lua.getTotalMemory - kernelMemory) / ramScale).toInt) - 1 - }) - lua.setField(-2, "totalMemory") - - lua.pushScalaFunction(lua => { - lua.pushBoolean(signal(lua.checkString(1), lua.toSimpleJavaObjects(2): _*)) - 1 - }) - lua.setField(-2, "pushSignal") - - // And its ROM address. - lua.pushScalaFunction(lua => { - rom.foreach(rom => Option(rom.node.address) match { - case None => lua.pushNil() - case Some(address) => lua.pushString(address) - }) - 1 - }) - lua.setField(-2, "romAddress") - - // And it's /tmp address... - lua.pushScalaFunction(lua => { - tmp.foreach(tmp => Option(tmp.node.address) match { - case None => lua.pushNil() - case Some(address) => lua.pushString(address) - }) - 1 - }) - lua.setField(-2, "tmpAddress") - - // User management. - lua.pushScalaFunction(lua => { - _users.foreach(lua.pushString) - _users.size - }) - lua.setField(-2, "users") - - lua.pushScalaFunction(lua => try { - if (_users.size >= Settings.get.maxUsers) - throw new Exception("too many users") - - val name = lua.checkString(1) - - if (_users.contains(name)) - throw new Exception("user exists") - if (name.length > Settings.get.maxUsernameLength) - throw new Exception("username too long") - if (!MinecraftServer.getServer.getConfigurationManager.getAllUsernames.contains(name)) - throw new Exception("player must be online") - - _users.synchronized { - _users += name - usersChanged = true - } - lua.pushBoolean(true) - 1 - } catch { - case e: Throwable => - lua.pushNil() - lua.pushString(Option(e.getMessage).getOrElse(e.toString)) - 2 - }) - lua.setField(-2, "addUser") - - lua.pushScalaFunction(lua => { - val name = lua.checkString(1) - _users.synchronized { - val success = _users.remove(name) - if (success) { - usersChanged = true - } - lua.pushBoolean(success) - } - 1 - }) - lua.setField(-2, "removeUser") - - lua.pushScalaFunction(lua => { - lua.pushNumber(node.globalBuffer) - 1 - }) - lua.setField(-2, "energy") - - lua.pushScalaFunction(lua => { - lua.pushNumber(node.globalBufferSize) - 1 - }) - lua.setField(-2, "maxEnergy") - - // Set the computer table. - lua.setGlobal("computer") - - // Until we get to ingame screens we log to Java's stdout. - lua.pushScalaFunction(lua => { - println((1 to lua.getTop).map(i => lua.`type`(i) match { - case LuaType.NIL => "nil" - case LuaType.BOOLEAN => lua.toBoolean(i) - case LuaType.NUMBER => lua.toNumber(i) - case LuaType.STRING => lua.toString(i) - case LuaType.TABLE => "table" - case LuaType.FUNCTION => "function" - case LuaType.THREAD => "thread" - case LuaType.LIGHTUSERDATA | LuaType.USERDATA => "userdata" - }).mkString(" ")) - 0 - }) - lua.setGlobal("print") - - // Whether bytecode may be loaded directly. - lua.pushScalaFunction(lua => { - lua.pushBoolean(Settings.get.allowBytecode) - 1 - }) - lua.setGlobal("allowBytecode") - - // How long programs may run without yielding before we stop them. - lua.pushNumber(Settings.get.timeout) - lua.setGlobal("timeout") - - // Component interaction stuff. - lua.newTable() - - lua.pushScalaFunction(lua => components.synchronized { - val filter = if (lua.isString(1)) Option(lua.toString(1)) else None - lua.newTable(0, components.size) - for ((address, name) <- components) { - if (filter.isEmpty || name.contains(filter.get)) { - lua.pushString(address) - lua.pushString(name) - lua.rawSet(-3) - } - } - 1 - }) - lua.setField(-2, "list") - - lua.pushScalaFunction(lua => components.synchronized { - components.get(lua.checkString(1)) match { - case Some(name: String) => - lua.pushString(name) - 1 - case _ => - lua.pushNil() - lua.pushString("no such component") - 2 - } - }) - lua.setField(-2, "type") - - lua.pushScalaFunction(lua => { - Option(node.network.node(lua.checkString(1))) match { - case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => - lua.newTable() - for (method <- component.methods()) { - lua.pushString(method) - lua.pushBoolean(component.isDirect(method)) - lua.rawSet(-3) - } - 1 - case _ => - lua.pushNil() - lua.pushString("no such component") - 2 - } - }) - lua.setField(-2, "methods") - - class LimitReachedException extends Exception - - lua.pushScalaFunction(lua => { - val address = lua.checkString(1) - val method = lua.checkString(2) - val args = lua.toSimpleJavaObjects(3) - try { - (Option(node.network.node(address)) match { - case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => - val direct = component.isDirect(method) - if (direct) callCounts.synchronized { - val limit = component.limit(method) - val counts = callCounts.getOrElseUpdate(component.address, mutable.Map.empty[String, Int]) - val count = counts.getOrElseUpdate(method, 0) - if (count >= limit) { - throw new LimitReachedException() - } - counts(method) += 1 - } - component.invoke(method, this, args: _*) - case _ => throw new Exception("no such component") - }) match { - case results: Array[_] => - lua.pushBoolean(true) - results.foreach(result => lua.pushValue(result)) - 1 + results.length - case _ => - lua.pushBoolean(true) - 1 - } - } - catch { - case e: Throwable => - if (Settings.get.logLuaCallbackErrors && !e.isInstanceOf[LimitReachedException]) { - OpenComputers.log.log(Level.WARNING, "Exception in Lua callback.", e) - } - e match { - case _: LimitReachedException => - 0 - case e: IllegalArgumentException if e.getMessage != null => - lua.pushBoolean(false) - lua.pushString(e.getMessage) - 2 - case e: Throwable if e.getMessage != null => - lua.pushBoolean(true) - lua.pushNil() - lua.pushString(e.getMessage) - if (Settings.get.logLuaCallbackErrors) { - lua.pushString(e.getStackTraceString.replace("\r\n", "\n")) - 4 - } - else 3 - case _: IndexOutOfBoundsException => - lua.pushBoolean(false) - lua.pushString("index out of bounds") - 2 - case _: IllegalArgumentException => - lua.pushBoolean(false) - lua.pushString("bad argument") - 2 - case _: NoSuchMethodException => - lua.pushBoolean(false) - lua.pushString("no such method") - 2 - case _: FileNotFoundException => - lua.pushBoolean(true) - lua.pushNil() - lua.pushString("file not found") - 3 - case _: SecurityException => - lua.pushBoolean(true) - lua.pushNil() - lua.pushString("access denied") - 3 - case _: IOException => - lua.pushBoolean(true) - lua.pushNil() - lua.pushString("i/o error") - 3 - case e: Throwable => - OpenComputers.log.log(Level.WARNING, "Unexpected error in Lua callback.", e) - lua.pushBoolean(true) - lua.pushNil() - lua.pushString("unknown error") - 3 - } - } - }) - lua.setField(-2, "invoke") - - lua.setGlobal("component") - - initPerms() - - lua.load(classOf[Machine].getResourceAsStream(Settings.scriptPath + "kernel.lua"), "=kernel", "t") - lua.newThread() // Left as the first value on the stack. - - // Clear any left-over signals from a previous run. - signals.clear() - - return true + return architecture.init() } catch { case ex: Throwable => @@ -1085,85 +616,11 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi false } - private def initPerms() { - // 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). - lua.newTable() /* ... perms */ - lua.newTable() /* ... uperms */ - - val perms = lua.getTop - 1 - val uperms = lua.getTop - - def flattenAndStore() { - /* ... k v */ - // We only care for tables and functions, any value types are safe. - if (lua.isFunction(-1) || lua.isTable(-1)) { - lua.pushValue(-2) /* ... k v k */ - lua.getTable(uperms) /* ... k v uperms[k] */ - assert(lua.isNil(-1), "duplicate permanent value named " + lua.toString(-3)) - lua.pop(1) /* ... k v */ - // If we have aliases its enough to store the value once. - lua.pushValue(-1) /* ... k v v */ - lua.getTable(perms) /* ... k v perms[v] */ - val isNew = lua.isNil(-1) - lua.pop(1) /* ... k v */ - if (isNew) { - lua.pushValue(-1) /* ... k v v */ - lua.pushValue(-3) /* ... k v v k */ - lua.rawSet(perms) /* ... k v ; perms[v] = k */ - lua.pushValue(-2) /* ... k v k */ - lua.pushValue(-2) /* ... k v k v */ - lua.rawSet(uperms) /* ... k v ; uperms[k] = v */ - // Recurse into tables. - if (lua.isTable(-1)) { - // Enforce a deterministic order when determining the keys, to ensure - // the keys are the same when unpersisting again. - val key = lua.toString(-2) - val childKeys = mutable.ArrayBuffer.empty[String] - lua.pushNil() /* ... k v nil */ - while (lua.next(-2)) { - /* ... k v ck cv */ - lua.pop(1) /* ... k v ck */ - childKeys += lua.toString(-1) - } - /* ... k v */ - childKeys.sortWith((a, b) => a.compareTo(b) < 0) - for (childKey <- childKeys) { - lua.pushString(key + "." + childKey) /* ... k v ck */ - lua.getField(-2, childKey) /* ... k v ck cv */ - flattenAndStore() /* ... k v */ - } - /* ... k v */ - } - /* ... k v */ - } - /* ... k v */ - } - lua.pop(2) /* ... */ - } - - // Mark everything that's globally reachable at this point as permanent. - lua.pushString("_G") /* ... perms uperms k */ - lua.getGlobal("_G") /* ... perms uperms k v */ - - flattenAndStore() /* ... perms uperms */ - lua.setField(LuaState.REGISTRYINDEX, "uperms") /* ... perms */ - lua.setField(LuaState.REGISTRYINDEX, "perms") /* ... */ - } - private def close() = state.synchronized( if (state.size == 0 || state.top != Machine.State.Stopped) { state.clear() state.push(Machine.State.Stopped) - if (lua != null) { - lua.setTotalMemory(Integer.MAX_VALUE) - lua.close() - } - lua = null - kernelMemory = 0 + architecture.close() signals.clear() timeStarted = 0 cpuTime = 0 @@ -1211,155 +668,62 @@ class Machine(val owner: Machine.Owner) extends ManagedComponent with Context wi switchTo(Machine.State.Running) } + cpuStart = System.nanoTime() + try { - // The kernel thread will always be at stack index one. - assert(lua.isThread(1)) + val result = architecture.runThreaded(enterState) - if (Settings.get.activeGC) { - // Help out the GC a little. The emergency GC has a few limitations - // that will make it free less memory than doing a full step manually. - lua.gc(LuaState.GcAction.COLLECT, 0) - } - - // Resume the Lua state and remember the number of results we get. - cpuStart = System.nanoTime() - val (results, runtime) = enterState match { - case Machine.State.SynchronizedReturn => - // If we were doing a synchronized call, continue where we left off. - assert(lua.getTop == 2) - assert(lua.isTable(2)) - (lua.resume(1, 1), System.nanoTime() - cpuStart) - case Machine.State.Yielded => - if (kernelMemory == 0) { - // We're doing the initialization run. - if (lua.resume(1, 0) > 0) { - // We expect to get nothing here, if we do we had an error. - (0, 0L) - } - else { - // 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 (including libraries). We remember - // that size to grant user-space programs a 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) - kernelMemory = math.max(lua.getTotalMemory - lua.getFreeMemory, 1) - recomputeMemory() - - // Fake zero sleep to avoid stopping if there are no signals. - lua.pushInteger(0) - (1, 0L) - } - } - else (signals.synchronized(if (signals.isEmpty) None else Some(signals.dequeue())) match { - case Some(signal) => - lua.pushString(signal.name) - signal.args.foreach(arg => lua.pushValue(arg)) - lua.resume(1, 1 + signal.args.length) - case _ => - lua.resume(1, 0) - }, System.nanoTime() - cpuStart) - case s => throw new AssertionError("Running computer from invalid state " + s.toString) - } - - // Keep track of time spent executing the computer. - cpuTime += runtime - - // Check if the kernel is still alive. - state.synchronized(if (lua.status(1) == LuaState.YIELD) { - // Check if someone called pause() or stop() in the meantime. - state.top match { - case Machine.State.Running => - // If we get one function it must be a wrapper for a synchronized - // call. The protocol is that a closure is pushed that is then called - // from the main server thread, and returns a table, which is in turn - // passed to the originating coroutine.yield(). - if (results == 1 && lua.isFunction(2)) { - switchTo(Machine.State.SynchronizedCall) - } - // Check if we are shutting down, and if so if we're rebooting. This - // is signalled by boolean values, where `false` means shut down, - // `true` means reboot (i.e shutdown then start again). - else if (results == 1 && lua.isBoolean(2)) { - if (lua.toBoolean(2)) switchTo(Machine.State.Restarting) - else switchTo(Machine.State.Stopping) - } - else { - // If we have a single number, that's how long we may wait before - // resuming the state again. Note that the sleep may be interrupted - // early if a signal arrives in the meantime. If we have something - // else we just process the next signal or wait for one. - val sleep = - if (results == 1 && lua.isNumber(2)) (lua.toNumber(2) * 20).toInt - else Int.MaxValue - lua.pop(results) + // Check if someone called pause() or stop() in the meantime. + state.synchronized(state.top match { + case Machine.State.Running => + result match { + case result: ExecutionResult.Sleep => signals.synchronized { // Immediately check for signals to allow processing more than one // signal per game tick. - if (signals.isEmpty && sleep > 0) { + if (signals.isEmpty && result.ticks > 0) { switchTo(Machine.State.Sleeping) - remainIdle = sleep + remainIdle = result.ticks } else { switchTo(Machine.State.Yielded) } } - } - case Machine.State.Paused => - state.pop() // Paused - state.pop() // Running, no switchTo to avoid new future. - state.push(Machine.State.Yielded) - state.push(Machine.State.Paused) - case Machine.State.Stopping => // Nothing to do, we'll die anyway. - case _ => throw new AssertionError( - "Invalid state in executor post-processing.") - } - } - // The kernel thread returned. If it threw we'd be in the catch below. - else { - assert(lua.isThread(1)) - // We're expecting the result of a pcall, if anything, so boolean + (result | string). - if (!lua.isBoolean(2) || !(lua.isString(3) || lua.isNil(3))) { - OpenComputers.log.warning("Kernel returned unexpected results.") - } - // The pcall *should* never return normally... but check for it nonetheless. - if (lua.toBoolean(2)) { - OpenComputers.log.warning("Kernel stopped unexpectedly.") - stop() - } - else { - lua.setTotalMemory(Int.MaxValue) - val error = lua.toString(3) - if (error != null) crash(error) - else crash("unknown error") - } + case result: ExecutionResult.SynchronizedCall => + switchTo(Machine.State.SynchronizedCall) + case result: ExecutionResult.Shutdown => + if (result.reboot) { + switchTo(Machine.State.Restarting) + } + else { + switchTo(Machine.State.Stopping) + } + case result: ExecutionResult.Error => + } + case Machine.State.Paused => + state.pop() // Paused + state.pop() // Running, no switchTo to avoid new future. + state.push(Machine.State.Yielded) + state.push(Machine.State.Paused) + case Machine.State.Stopping => // Nothing to do, we'll die anyway. + case _ => throw new AssertionError( + "Invalid state in executor post-processing.") }) } catch { - case e: LuaRuntimeException => - OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) - crash("kernel panic: this is a bug, check your log file and report it") - case e: LuaGcMetamethodException => - if (e.getMessage != null) crash("kernel panic:\n" + e.getMessage) - else crash("kernel panic:\nerror in garbage collection metamethod") - case e: LuaMemoryAllocationException => - crash("not enough memory") - case e: java.lang.Error if e.getMessage == "not enough memory" => - crash("not enough memory") - case e: Throwable => - OpenComputers.log.log(Level.WARNING, "Unexpected error in kernel. This is a bug!\n", e) - crash("kernel panic: this is a bug, check your log file and report it") + case e: Throwable => OpenComputers.log.log(Level.WARNING, "Architecture's runThreaded threw an error. This should never happen!", e) } + + // Keep track of time spent executing the computer. + cpuTime += System.nanoTime() - cpuStart } } object Machine { - /** Signals are messages sent to the Lua state from Java asynchronously. */ - private class Signal(val name: String, val args: Array[Any]) + private[component] class LimitReachedException extends Exception /** Possible states of the computer, and in particular its executor. */ - private object State extends Enumeration { + private[component] object State extends Enumeration { /** The computer is not running right now and there is no Lua state. */ val Stopped = Value("Stopped") @@ -1391,7 +755,10 @@ object Machine { val Running = Value("Running") } - private val threadPool = ThreadPoolFactory.create("Lua", Settings.get.threads) + /** Signals are messages sent to the Lua state from Java asynchronously. */ + private[component] class Signal(val name: String, val args: Array[Any]) + + private val threadPool = ThreadPoolFactory.create("Computer", Settings.get.threads) trait Owner { def installedMemory: Int diff --git a/li/cil/oc/server/component/machine/Architecture.scala b/li/cil/oc/server/component/machine/Architecture.scala new file mode 100644 index 000000000..a30396780 --- /dev/null +++ b/li/cil/oc/server/component/machine/Architecture.scala @@ -0,0 +1,93 @@ +package li.cil.oc.server.component.machine + +import li.cil.oc.server.component.Machine +import net.minecraft.nbt.NBTTagCompound + +/** + * This trait abstracts away any language specific details for the Machine. + * + * At some point in the future this may allow us to introduce other languages, + * e.g. computers that run assembly or non-persistent computers that use a pure + * Java implementation of Lua. + */ +trait Architecture { + /** + * Used to check if the machine is fully initialized. If this is false no + * signals for detected components will be generated. Avoids duplicate signals + * if component_added signals are generated in the language's startup script, + * for already present components (see Lua's init.lua script). + * + * @return whether the machine is fully initialized. + */ + def isInitialized: Boolean + + /** + * This is called when the amount of memory in the computer may have changed. + * It is triggered by the tile entity's onInventoryChanged. + */ + def recomputeMemory() + + /** + * Performs a synchronized call initialized in a previous call to runThreaded. + */ + def runSynchronized() + + /** + * Continues execution of the machine. The first call may be used to + * initialize the machine (e.g. for Lua we load the libraries in the first + * call so that the computers boot faster). + * + * The resumed state is either Machine.State.SynchronizedReturn, when a + * synchronized call has been completed (via runSynchronized), or + * Machine.State.Yielded in all other cases (sleep, interrupt, boot, ...). + * + * This is expected to return within a very short time, usually. For example, + * in Lua this returns as soon as the state yields, and returns at the latest + * when the Settings.timeout is reached (in which case it forces the state + * to crash). + * + * This is expected to consume a single signal if one is present and return. + * If returning from a synchronized call this should consume no signal. + * + * @param enterState the state that is being resumed. + * @return the result of the execution. Used to determine the new state. + */ + def runThreaded(enterState: Machine.State.Value): ExecutionResult + + /** + * Called when a computer starts up. Used to (re-)initialize the underlying + * architecture logic. For example, for Lua the creates a new Lua state. + * + * This also sets up any built-in APIs for the underlying language, such as + * querying available memory, listing and interacting with components and so + * on. If this returns false the computer fails to start. + * + * @return whether the architecture was initialized successfully. + */ + def init(): Boolean + + /** + * Called when a computer stopped. Used to clean up any handles, memory and + * so on. For example, for Lua this destroys the Lua state. + */ + def close() + + /** + * Restores the state of this architecture as previously saved in save(). + * + * @param nbt the tag compound to save to. + */ + def load(nbt: NBTTagCompound) + + /** + * Saves the architecture for later restoration, e.g. across games or chunk + * unloads. Used to persist a computer's executions state. For native Lua this + * uses the Eris library to persist the main coroutine. + * + * Note that the tag compound is shared with the Machine, so collisions have + * to be avoided (see Machine.save for used keys). + * + * @param nbt the tag compound to save to. + */ + def save(nbt: NBTTagCompound) +} diff --git a/li/cil/oc/server/component/machine/ExecutionResult.scala b/li/cil/oc/server/component/machine/ExecutionResult.scala new file mode 100644 index 000000000..fbdcfa873 --- /dev/null +++ b/li/cil/oc/server/component/machine/ExecutionResult.scala @@ -0,0 +1,45 @@ +package li.cil.oc.server.component.machine + +/** + * Used by the Machine to determine the result of a call to runThreaded. + * + * Do not implement this interface, only use the predefined classes below. + */ +trait ExecutionResult + +object ExecutionResult { + + /** + * Indicates the machine may sleep for the specified number of ticks. This is + * merely considered a suggestion. If signals are in the queue or are pushed + * to the queue while sleeping, the sleep will be interrupted and runThreaded + * will be called so that the next signal is pushed. + * + * @param ticks the number of ticks to sleep. + */ + class Sleep(val ticks: Int) extends ExecutionResult + + /** + * Indicates tha the computer should shutdown or reboot. + * + * @param reboot whether to reboot. If false the computer will stop. + */ + class Shutdown(val reboot: Boolean) extends ExecutionResult + + /** + * Indicates that a synchronized call should be performed. The architecture + * is expected to be in a state that allows the next call to be to + * runSynchronized instead of runThreaded. This is used to perform calls from + * the server's main thread, to avoid threading issues when interacting with + * other objects in the world. + */ + class SynchronizedCall extends ExecutionResult + + /** + * Indicates that an error occurred and the computer should crash. + * + * @param message the error message. + */ + class Error(val message: String) extends ExecutionResult + +} \ No newline at end of file diff --git a/li/cil/oc/server/component/machine/LuaArchitecture.scala b/li/cil/oc/server/component/machine/LuaArchitecture.scala new file mode 100644 index 000000000..2db3990bb --- /dev/null +++ b/li/cil/oc/server/component/machine/LuaArchitecture.scala @@ -0,0 +1,715 @@ +package li.cil.oc.server.component.machine + +import com.naef.jnlua._ +import java.io.{IOException, FileNotFoundException} +import java.util.logging.Level +import li.cil.oc.server.component.Machine +import li.cil.oc.util.ExtendedLuaState.extendLuaState +import li.cil.oc.util.{GameTimeFormatter, LuaStateFactory} +import li.cil.oc.{OpenComputers, server, Settings} +import net.minecraft.nbt.NBTTagCompound +import scala.Some +import scala.collection.convert.WrapAsScala._ +import scala.collection.mutable + +class LuaArchitecture(val machine: Machine) extends Architecture { + private var lua: LuaState = null + + private var kernelMemory = 0 + + private val ramScale = if (LuaStateFactory.is64Bit) Settings.get.ramScaleFor64Bit else 1.0 + + private def node = machine.node + + private def state = machine.state + + private def components = machine.components + + def isInitialized = kernelMemory > 0 + + def recomputeMemory() = Option(lua) match { + case Some(l) => + l.setTotalMemory(Int.MaxValue) + l.gc(LuaState.GcAction.COLLECT, 0) + if (kernelMemory > 0) { + l.setTotalMemory(kernelMemory + math.ceil(machine.owner.installedMemory * ramScale).toInt) + } + case _ => + } + + def runSynchronized() { + // These three asserts are all guaranteed by run(). + assert(lua.getTop == 2) + assert(lua.isThread(1)) + assert(lua.isFunction(2)) + + try { + // Synchronized call protocol requires the called function to return + // a table, which holds the results of the call, to be passed back + // to the coroutine.yield() that triggered the call. + lua.call(0, 1) + lua.checkType(2, LuaType.TABLE) + } + catch { + case _: LuaMemoryAllocationException => + // This can happen if we run out of memory while converting a Java + // exception to a string (which we have to do to avoid keeping + // userdata on the stack, which cannot be persisted). + throw new java.lang.OutOfMemoryError("not enough memory") + } + } + + def runThreaded(enterState: Machine.State.Value): ExecutionResult = { + try { + // The kernel thread will always be at stack index one. + assert(lua.isThread(1)) + + if (Settings.get.activeGC) { + // Help out the GC a little. The emergency GC has a few limitations + // that will make it free less memory than doing a full step manually. + lua.gc(LuaState.GcAction.COLLECT, 0) + } + + // Resume the Lua state and remember the number of results we get. + val results = enterState match { + case Machine.State.SynchronizedReturn => + // If we were doing a synchronized call, continue where we left off. + assert(lua.getTop == 2) + assert(lua.isTable(2)) + lua.resume(1, 1) + case Machine.State.Yielded => + if (kernelMemory == 0) { + // We're doing the initialization run. + if (lua.resume(1, 0) > 0) { + // We expect to get nothing here, if we do we had an error. + 0 + } + else { + // 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 (including libraries). We remember + // that size to grant user-space programs a 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) + kernelMemory = math.max(lua.getTotalMemory - lua.getFreeMemory, 1) + recomputeMemory() + + // Fake zero sleep to avoid stopping if there are no signals. + lua.pushInteger(0) + 1 + } + } + else machine.popSignal() match { + case Some(signal) => + lua.pushString(signal.name) + signal.args.foreach(arg => lua.pushValue(arg)) + lua.resume(1, 1 + signal.args.length) + case _ => + lua.resume(1, 0) + } + case s => throw new AssertionError("Running computer from invalid state " + s.toString) + } + + // Check if the kernel is still alive. + if (lua.status(1) == LuaState.YIELD) { + // If we get one function it must be a wrapper for a synchronized + // call. The protocol is that a closure is pushed that is then called + // from the main server thread, and returns a table, which is in turn + // passed to the originating coroutine.yield(). + if (results == 1 && lua.isFunction(2)) { + new ExecutionResult.SynchronizedCall() + } + // Check if we are shutting down, and if so if we're rebooting. This + // is signalled by boolean values, where `false` means shut down, + // `true` means reboot (i.e shutdown then start again). + else if (results == 1 && lua.isBoolean(2)) { + new ExecutionResult.Shutdown(lua.toBoolean(2)) + } + else { + // If we have a single number, that's how long we may wait before + // resuming the state again. Note that the sleep may be interrupted + // early if a signal arrives in the meantime. If we have something + // else we just process the next signal or wait for one. + val ticks = if (results == 1 && lua.isNumber(2)) (lua.toNumber(2) * 20).toInt else Int.MaxValue + lua.pop(results) + new ExecutionResult.Sleep(ticks) + } + } + // The kernel thread returned. If it threw we'd be in the catch below. + else { + assert(lua.isThread(1)) + // We're expecting the result of a pcall, if anything, so boolean + (result | string). + if (!lua.isBoolean(2) || !(lua.isString(3) || lua.isNil(3))) { + OpenComputers.log.warning("Kernel returned unexpected results.") + } + // The pcall *should* never return normally... but check for it nonetheless. + if (lua.toBoolean(2)) { + OpenComputers.log.warning("Kernel stopped unexpectedly.") + new ExecutionResult.Shutdown(false) + } + else { + lua.setTotalMemory(Int.MaxValue) + val error = lua.toString(3) + if (error != null) new ExecutionResult.Error(error) + else new ExecutionResult.Error("unknown error") + } + } + } + catch { + case e: LuaRuntimeException => + OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) + new ExecutionResult.Error("kernel panic: this is a bug, check your log file and report it") + case e: LuaGcMetamethodException => + if (e.getMessage != null) new ExecutionResult.Error("kernel panic:\n" + e.getMessage) + else new ExecutionResult.Error("kernel panic:\nerror in garbage collection metamethod") + case e: LuaMemoryAllocationException => + new ExecutionResult.Error("not enough memory") + case e: java.lang.Error if e.getMessage == "not enough memory" => + new ExecutionResult.Error("not enough memory") + case e: Throwable => + OpenComputers.log.log(Level.WARNING, "Unexpected error in kernel. This is a bug!\n", e) + new ExecutionResult.Error("kernel panic: this is a bug, check your log file and report it") + } + } + + def init(): Boolean = { + // 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. + LuaStateFactory.createState() match { + case None => + lua = null + machine.message = Some("native libraries not available") + return false + case Some(value) => lua = value + } + + // Push a couple of functions that override original Lua API functions or + // that add new functionality to it. + lua.getGlobal("os") + + // Custom os.clock() implementation returning the time the computer has + // been actively running, instead of the native library... + lua.pushScalaFunction(lua => { + lua.pushNumber((machine.cpuTime + (System.nanoTime() - machine.cpuStart)) * 10e-10) + 1 + }) + lua.setField(-2, "clock") + + // Date formatting function. + lua.pushScalaFunction(lua => { + val format = + if (lua.getTop > 0 && lua.isString(1)) lua.toString(1) + else "%d/%m/%y %H:%M:%S" + val time = + if (lua.getTop > 1 && lua.isNumber(2)) lua.toNumber(2) * 1000 / 60 / 60 + else machine.worldTime + 6000 + + val dt = GameTimeFormatter.parse(time) + def fmt(format: String) { + if (format == "*t") { + lua.newTable(0, 8) + lua.pushInteger(dt.year) + lua.setField(-2, "year") + lua.pushInteger(dt.month) + lua.setField(-2, "month") + lua.pushInteger(dt.day) + lua.setField(-2, "day") + lua.pushInteger(dt.hour) + lua.setField(-2, "hour") + lua.pushInteger(dt.minute) + lua.setField(-2, "min") + lua.pushInteger(dt.second) + lua.setField(-2, "sec") + lua.pushInteger(dt.weekDay) + lua.setField(-2, "wday") + lua.pushInteger(dt.yearDay) + lua.setField(-2, "yday") + } + else { + lua.pushString(GameTimeFormatter.format(format, dt)) + } + } + + // Just ignore the allowed leading '!', Minecraft has no time zones... + if (format.startsWith("!")) + fmt(format.substring(1)) + else + fmt(format) + 1 + }) + lua.setField(-2, "date") + + // Return ingame time for os.time(). + lua.pushScalaFunction(lua => { + // Game time is in ticks, so that each day has 24000 ticks, meaning + // one hour is game time divided by one thousand. Also, Minecraft + // starts days at 6 o'clock, so we add those six hours. Thus: + // timestamp = (time + 6000) * 60[kh] * 60[km] / 1000[s] + lua.pushNumber((machine.worldTime + 6000) * 60 * 60 / 1000) + 1 + }) + lua.setField(-2, "time") + + // Pop the os table. + lua.pop(1) + + // Computer API, stuff that kinda belongs to os, but we don't want to + // clutter it. + lua.newTable() + + // Allow getting the real world time for timeouts. + lua.pushScalaFunction(lua => { + lua.pushNumber(System.currentTimeMillis() / 1000.0) + 1 + }) + lua.setField(-2, "realTime") + + // The time the computer has been running, as opposed to the CPU time. + lua.pushScalaFunction(lua => { + // World time is in ticks, and each second has 20 ticks. Since we + // want uptime() to return real seconds, though, we'll divide it + // accordingly. + lua.pushNumber((machine.worldTime - machine.timeStarted) / 20.0) + 1 + }) + lua.setField(-2, "uptime") + + // Allow the computer to figure out its own id in the component network. + lua.pushScalaFunction(lua => { + Option(node.address) match { + case None => lua.pushNil() + case Some(address) => lua.pushString(address) + } + 1 + }) + lua.setField(-2, "address") + + // Are we a robot? (No this is not a CAPTCHA.) + lua.pushScalaFunction(lua => { + lua.pushBoolean(machine.isRobot) + 1 + }) + lua.setField(-2, "isRobot") + + lua.pushScalaFunction(lua => { + // This is *very* unlikely, but still: avoid this getting larger than + // what we report as the total memory. + lua.pushInteger(((lua.getFreeMemory min (lua.getTotalMemory - kernelMemory)) / ramScale).toInt) + 1 + }) + lua.setField(-2, "freeMemory") + + // Allow the system to read how much memory it uses and has available. + lua.pushScalaFunction(lua => { + lua.pushInteger(((lua.getTotalMemory - kernelMemory) / ramScale).toInt) + 1 + }) + lua.setField(-2, "totalMemory") + + lua.pushScalaFunction(lua => { + lua.pushBoolean(machine.signal(lua.checkString(1), lua.toSimpleJavaObjects(2): _*)) + 1 + }) + lua.setField(-2, "pushSignal") + + // And its ROM address. + lua.pushScalaFunction(lua => { + machine.rom.foreach(rom => Option(rom.node.address) match { + case None => lua.pushNil() + case Some(address) => lua.pushString(address) + }) + 1 + }) + lua.setField(-2, "romAddress") + + // And it's /tmp address... + lua.pushScalaFunction(lua => { + machine.tmp.foreach(tmp => Option(tmp.node.address) match { + case None => lua.pushNil() + case Some(address) => lua.pushString(address) + }) + 1 + }) + lua.setField(-2, "tmpAddress") + + // User management. + lua.pushScalaFunction(lua => { + val users = machine.users + users.foreach(lua.pushString) + users.length + }) + lua.setField(-2, "users") + + lua.pushScalaFunction(lua => try { + machine.addUser(lua.checkString(1)) + lua.pushBoolean(true) + 1 + } catch { + case e: Throwable => + lua.pushNil() + lua.pushString(Option(e.getMessage).getOrElse(e.toString)) + 2 + }) + lua.setField(-2, "addUser") + + lua.pushScalaFunction(lua => { + lua.pushBoolean(machine.removeUser(lua.checkString(1))) + 1 + }) + lua.setField(-2, "removeUser") + + lua.pushScalaFunction(lua => { + lua.pushNumber(node.globalBuffer) + 1 + }) + lua.setField(-2, "energy") + + lua.pushScalaFunction(lua => { + lua.pushNumber(node.globalBufferSize) + 1 + }) + lua.setField(-2, "maxEnergy") + + // Set the computer table. + lua.setGlobal("computer") + + // Until we get to ingame screens we log to Java's stdout. + lua.pushScalaFunction(lua => { + println((1 to lua.getTop).map(i => lua.`type`(i) match { + case LuaType.NIL => "nil" + case LuaType.BOOLEAN => lua.toBoolean(i) + case LuaType.NUMBER => lua.toNumber(i) + case LuaType.STRING => lua.toString(i) + case LuaType.TABLE => "table" + case LuaType.FUNCTION => "function" + case LuaType.THREAD => "thread" + case LuaType.LIGHTUSERDATA | LuaType.USERDATA => "userdata" + }).mkString(" ")) + 0 + }) + lua.setGlobal("print") + + // Whether bytecode may be loaded directly. + lua.pushScalaFunction(lua => { + lua.pushBoolean(Settings.get.allowBytecode) + 1 + }) + lua.setGlobal("allowBytecode") + + // How long programs may run without yielding before we stop them. + lua.pushNumber(Settings.get.timeout) + lua.setGlobal("timeout") + + // Component interaction stuff. + lua.newTable() + + lua.pushScalaFunction(lua => components.synchronized { + val filter = if (lua.isString(1)) Option(lua.toString(1)) else None + lua.newTable(0, components.size) + for ((address, name) <- components) { + if (filter.isEmpty || name.contains(filter.get)) { + lua.pushString(address) + lua.pushString(name) + lua.rawSet(-3) + } + } + 1 + }) + lua.setField(-2, "list") + + lua.pushScalaFunction(lua => components.synchronized { + components.get(lua.checkString(1)) match { + case Some(name: String) => + lua.pushString(name) + 1 + case _ => + lua.pushNil() + lua.pushString("no such component") + 2 + } + }) + lua.setField(-2, "type") + + lua.pushScalaFunction(lua => { + Option(node.network.node(lua.checkString(1))) match { + case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => + lua.newTable() + for (method <- component.methods()) { + lua.pushString(method) + lua.pushBoolean(component.isDirect(method)) + lua.rawSet(-3) + } + 1 + case _ => + lua.pushNil() + lua.pushString("no such component") + 2 + } + }) + lua.setField(-2, "methods") + + lua.pushScalaFunction(lua => { + val address = lua.checkString(1) + val method = lua.checkString(2) + val args = lua.toSimpleJavaObjects(3) + try { + machine.invoke(address, method, args) match { + case results: Array[_] => + lua.pushBoolean(true) + results.foreach(result => lua.pushValue(result)) + 1 + results.length + case _ => + lua.pushBoolean(true) + 1 + } + } + catch { + case e: Throwable => + if (Settings.get.logLuaCallbackErrors && !e.isInstanceOf[Machine.LimitReachedException]) { + OpenComputers.log.log(Level.WARNING, "Exception in Lua callback.", e) + } + e match { + case _: Machine.LimitReachedException => + 0 + case e: IllegalArgumentException if e.getMessage != null => + lua.pushBoolean(false) + lua.pushString(e.getMessage) + 2 + case e: Throwable if e.getMessage != null => + lua.pushBoolean(true) + lua.pushNil() + lua.pushString(e.getMessage) + if (Settings.get.logLuaCallbackErrors) { + lua.pushString(e.getStackTraceString.replace("\r\n", "\n")) + 4 + } + else 3 + case _: IndexOutOfBoundsException => + lua.pushBoolean(false) + lua.pushString("index out of bounds") + 2 + case _: IllegalArgumentException => + lua.pushBoolean(false) + lua.pushString("bad argument") + 2 + case _: NoSuchMethodException => + lua.pushBoolean(false) + lua.pushString("no such method") + 2 + case _: FileNotFoundException => + lua.pushBoolean(true) + lua.pushNil() + lua.pushString("file not found") + 3 + case _: SecurityException => + lua.pushBoolean(true) + lua.pushNil() + lua.pushString("access denied") + 3 + case _: IOException => + lua.pushBoolean(true) + lua.pushNil() + lua.pushString("i/o error") + 3 + case e: Throwable => + OpenComputers.log.log(Level.WARNING, "Unexpected error in Lua callback.", e) + lua.pushBoolean(true) + lua.pushNil() + lua.pushString("unknown error") + 3 + } + } + }) + lua.setField(-2, "invoke") + + lua.setGlobal("component") + + initPerms() + + lua.load(classOf[Machine].getResourceAsStream(Settings.scriptPath + "kernel.lua"), "=kernel", "t") + lua.newThread() // Left as the first value on the stack. + + true + } + + def close() { + if (lua != null) { + lua.setTotalMemory(Integer.MAX_VALUE) + lua.close() + } + lua = null + kernelMemory = 0 + } + + def load(nbt: NBTTagCompound) { + // Unlimit memory use while unpersisting. + lua.setTotalMemory(Integer.MAX_VALUE) + + try { + // Try unpersisting Lua, because that's what all of the rest depends + // on. First, clear the stack, meaning the current kernel. + lua.setTop(0) + + unpersist(nbt.getByteArray("kernel")) + if (!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 IllegalArgumentException("Invalid kernel.") + } + if (state.contains(Machine.State.SynchronizedCall) || state.contains(Machine.State.SynchronizedReturn)) { + unpersist(nbt.getByteArray("stack")) + if (!(if (state.contains(Machine.State.SynchronizedCall)) lua.isFunction(2) else lua.isTable(2))) { + // Same as with the above, should not really happen normally, but + // could for the same reasons. + throw new IllegalArgumentException("Invalid stack.") + } + } + + kernelMemory = (nbt.getInteger("kernelMemory") * ramScale).toInt + } catch { + case e: LuaRuntimeException => + OpenComputers.log.warning("Could not unpersist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) + state.push(Machine.State.Stopping) + } + + // Limit memory again. + recomputeMemory() + } + + def save(nbt: NBTTagCompound) { + // Unlimit memory while persisting. + lua.setTotalMemory(Integer.MAX_VALUE) + + try { + // Try persisting Lua, because that's what all of the rest depends on. + // Save the kernel state (which is always at stack index one). + assert(lua.isThread(1)) + nbt.setByteArray("kernel", persist(1)) + // 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.contains(Machine.State.SynchronizedCall) || state.contains(Machine.State.SynchronizedReturn)) { + assert(if (state.contains(Machine.State.SynchronizedCall)) lua.isFunction(2) else lua.isTable(2)) + nbt.setByteArray("stack", persist(2)) + } + + nbt.setInteger("kernelMemory", math.ceil(kernelMemory / ramScale).toInt) + } catch { + case e: LuaRuntimeException => + OpenComputers.log.warning("Could not persist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) + nbt.removeTag("state") + } + + // Limit memory again. + recomputeMemory() + } + + private def initPerms() { + // 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). + lua.newTable() /* ... perms */ + lua.newTable() /* ... uperms */ + + val perms = lua.getTop - 1 + val uperms = lua.getTop + + def flattenAndStore() { + /* ... k v */ + // We only care for tables and functions, any value types are safe. + if (lua.isFunction(-1) || lua.isTable(-1)) { + lua.pushValue(-2) /* ... k v k */ + lua.getTable(uperms) /* ... k v uperms[k] */ + assert(lua.isNil(-1), "duplicate permanent value named " + lua.toString(-3)) + lua.pop(1) /* ... k v */ + // If we have aliases its enough to store the value once. + lua.pushValue(-1) /* ... k v v */ + lua.getTable(perms) /* ... k v perms[v] */ + val isNew = lua.isNil(-1) + lua.pop(1) /* ... k v */ + if (isNew) { + lua.pushValue(-1) /* ... k v v */ + lua.pushValue(-3) /* ... k v v k */ + lua.rawSet(perms) /* ... k v ; perms[v] = k */ + lua.pushValue(-2) /* ... k v k */ + lua.pushValue(-2) /* ... k v k v */ + lua.rawSet(uperms) /* ... k v ; uperms[k] = v */ + // Recurse into tables. + if (lua.isTable(-1)) { + // Enforce a deterministic order when determining the keys, to ensure + // the keys are the same when unpersisting again. + val key = lua.toString(-2) + val childKeys = mutable.ArrayBuffer.empty[String] + lua.pushNil() /* ... k v nil */ + while (lua.next(-2)) { + /* ... k v ck cv */ + lua.pop(1) /* ... k v ck */ + childKeys += lua.toString(-1) + } + /* ... k v */ + childKeys.sortWith((a, b) => a.compareTo(b) < 0) + for (childKey <- childKeys) { + lua.pushString(key + "." + childKey) /* ... k v ck */ + lua.getField(-2, childKey) /* ... k v ck cv */ + flattenAndStore() /* ... k v */ + } + /* ... k v */ + } + /* ... k v */ + } + /* ... k v */ + } + lua.pop(2) /* ... */ + } + + // Mark everything that's globally reachable at this point as permanent. + lua.pushString("_G") /* ... perms uperms k */ + lua.getGlobal("_G") /* ... perms uperms k v */ + + flattenAndStore() /* ... perms uperms */ + lua.setField(LuaState.REGISTRYINDEX, "uperms") /* ... perms */ + lua.setField(LuaState.REGISTRYINDEX, "perms") /* ... */ + } + + private def persist(index: Int): Array[Byte] = { + lua.getGlobal("eris") /* ... eris */ + lua.getField(-1, "persist") /* ... eris persist */ + if (lua.isFunction(-1)) { + lua.getField(LuaState.REGISTRYINDEX, "perms") /* ... eris persist perms */ + lua.pushValue(index) // ... eris persist perms obj + try { + lua.call(2, 1) // ... eris str? + } catch { + case e: Throwable => + lua.pop(1) + throw e + } + if (lua.isString(-1)) { + // ... eris str + val result = lua.toByteArray(-1) + lua.pop(2) // ... + return result + } // ... eris :( + } // ... eris :( + lua.pop(2) // ... + Array[Byte]() + } + + private def unpersist(value: Array[Byte]): Boolean = { + lua.getGlobal("eris") // ... eris + lua.getField(-1, "unpersist") // ... eris unpersist + if (lua.isFunction(-1)) { + lua.getField(LuaState.REGISTRYINDEX, "uperms") /* ... eris persist uperms */ + lua.pushByteArray(value) // ... eris unpersist uperms str + lua.call(2, 1) // ... eris obj + lua.insert(-2) // ... obj eris + lua.pop(1) + return true + } // ... :( + lua.pop(1) + false + } +} \ No newline at end of file