| package li.cil.oc.server.component |
| |
| import com.naef.jnlua._ |
| import java.io.{FileNotFoundException, IOException} |
| import java.lang.Thread.UncaughtExceptionHandler |
| import java.util.concurrent._ |
| import java.util.concurrent.atomic.AtomicInteger |
| import java.util.logging.Level |
| import li.cil.oc.api |
| import li.cil.oc.api.Persistable |
| import li.cil.oc.api.network._ |
| import li.cil.oc.common.tileentity |
| import li.cil.oc.server |
| import li.cil.oc.util.ExtendedLuaState.extendLuaState |
| import li.cil.oc.util.{GameTimeFormatter, LuaStateFactory} |
| import li.cil.oc.{OpenComputers, Config} |
| import net.minecraft.entity.player.EntityPlayer |
| import net.minecraft.nbt._ |
| import net.minecraft.server.MinecraftServer |
| import scala.Array.canBuildFrom |
| import scala.Some |
| import scala.collection.convert.WrapAsScala._ |
| import scala.collection.mutable |
| import scala.math.ScalaNumber |
| import scala.runtime.BoxedUnit |
| |
| class Computer(val owner: tileentity.Computer) extends Environment with Context with Persistable with Runnable { |
| val node = api.Network.newNode(this, Visibility.Network). |
| withComponent("computer", Visibility.Neighbors). |
| withConnector(). |
| create() |
| |
| val rom = Option(api.FileSystem.asManagedEnvironment(api.FileSystem. |
| fromClass(OpenComputers.getClass, Config.resourceDomain, "lua/rom"), "rom")) |
| |
| val tmp = Option(api.FileSystem.asManagedEnvironment(api.FileSystem. |
| fromMemory(512 * 1024), "tmpfs")) |
| |
| private val state = mutable.Stack(Computer.State.Stopped) |
| |
| private var lua: LuaState = null |
| |
| private var kernelMemory = 0 |
| |
| private val components = mutable.Map.empty[String, String] |
| |
| private val addedComponents = mutable.Set.empty[Component] |
| |
| private val users = mutable.Set.empty[String] |
| |
| private val signals = new mutable.Queue[Computer.Signal] |
| |
| private var future: Option[Future[_]] = None |
| |
| private val callCounts = mutable.Map.empty[String, mutable.Map[String, Int]] |
| |
| // ----------------------------------------------------------------------- // |
| |
| private var timeStarted = 0L // Game-world time [ms] for os.uptime(). |
| |
| private var worldTime = 0L // Game-world time for os.time(). |
| |
| private var lastUpdate = 0L // Real-world time [ms] for pause detection. |
| |
| private var cpuTime = 0L // Pseudo-real-world time [ns] for os.clock(). |
| |
| private var cpuStart = 0L // Pseudo-real-world time [ns] for os.clock(). |
| |
| private var sleepUntil = 0.0 // Real-world time [ms]. |
| |
| private var message: Option[String] = None // For error messages. |
| |
| // ----------------------------------------------------------------------- // |
| |
| def recomputeMemory() = state.synchronized(if (lua != null) { |
| lua.setTotalMemory(Int.MaxValue) |
| lua.gc(LuaState.GcAction.COLLECT, 0) |
| if (kernelMemory > 0) |
| lua.setTotalMemory(kernelMemory + owner.installedMemory) |
| }) |
| |
| def lastError = message |
| |
| // ----------------------------------------------------------------------- // |
| |
| def isRunning = state.synchronized(state.top != Computer.State.Stopped) |
| |
| def isStopping = state.synchronized(state.top == Computer.State.Stopping) |
| |
| def start() = state.synchronized(node.network != null && |
| state.top == Computer.State.Stopped && |
| owner.installedMemory > 0 && |
| init() && { |
| // Initial state. Will be switched to State.Yielded in the next update(). |
| switchTo(Computer.State.Starting) |
| |
| // Remember when we started, for os.clock(). |
| timeStarted = owner.world.getWorldTime |
| |
| // Mark state change in owner, to send it to clients. |
| owner.markAsChanged() |
| |
| // All green, computer started successfully. |
| node.sendToReachable("computer.started") |
| true |
| }) |
| |
| def pause() = if (state.synchronized(isRunning && !isStopping)) { |
| this.synchronized(if (!isStopping) state.push(Computer.State.Paused)) |
| } |
| |
| def stop() = state.synchronized(state.top match { |
| case Computer.State.Stopped | Computer.State.Stopping => false |
| case _ => state.push(Computer.State.Stopping); true |
| }) |
| |
| def crash(message: String) = { |
| this.message = Option(message) |
| stop() |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def address = node.address |
| |
| def isUser(player: String) = !Config.canComputersBeOwned || |
| users.isEmpty || users.contains(player) || |
| MinecraftServer.getServer.isSinglePlayer || |
| MinecraftServer.getServer.getConfigurationManager.isPlayerOpped(player) |
| |
| def signal(name: String, args: AnyRef*) = state.synchronized(state.top match { |
| case Computer.State.Stopped | Computer.State.Stopping => false |
| case _ => signals.synchronized { |
| if (signals.size >= 256) false |
| else { |
| signals.enqueue(new Computer.Signal(name, args.map { |
| case null | Unit | None => Unit |
| case arg: java.lang.Boolean => arg |
| case arg: java.lang.Byte => arg.toDouble |
| case arg: java.lang.Character => arg.toDouble |
| case arg: java.lang.Short => arg.toDouble |
| case arg: java.lang.Integer => arg.toDouble |
| case arg: java.lang.Long => arg.toDouble |
| case arg: java.lang.Float => arg.toDouble |
| case arg: java.lang.Double => arg |
| case arg: java.lang.String => arg |
| case arg: Array[Byte] => arg |
| case arg => |
| OpenComputers.log.warning("Trying to push signal with an unsupported argument of type " + arg.getClass.getName) |
| Unit |
| }.toArray)) |
| true |
| } |
| } |
| }) |
| |
| // ----------------------------------------------------------------------- // |
| |
| def update() { |
| // Add components that were added since the last update to the actual list |
| // of components if we can see them. We use this delayed approach to avoid |
| // issues with components that have a visibility lower than their |
| // reachability, because in that case if they get connected in the wrong |
| // order we wouldn't add them (since they'd be invisible in their connect |
| // message, and only become visible with a later node-to-node connection, |
| // but that wouldn't trigger a connect message anymore due to the higher |
| // reachability). |
| processAddedComponents() |
| |
| // Update last time run to let our executor thread know it doesn't have to |
| // pause. |
| lastUpdate = System.currentTimeMillis |
| |
| // Update world time for time(). |
| worldTime = owner.world.getWorldTime |
| if (isRunning) { |
| // We can have rollbacks from '/time set'. Avoid getting negative uptimes. |
| timeStarted = timeStarted min worldTime |
| } |
| |
| // Clear direct call limits. |
| callCounts.synchronized(callCounts.clear()) |
| |
| // Make sure we have enough power. |
| if (isRunning && !isStopping && !node.changeBuffer(-Config.computerCost)) { |
| crash("not enough energy") |
| } |
| |
| // Check if we should switch states. These are all the states in which we're |
| // guaranteed that the executor thread isn't running anymore. |
| state.synchronized(state.top match { |
| // Booting up. |
| case Computer.State.Starting => { |
| verifyComponents() |
| switchTo(Computer.State.Yielded) |
| } |
| // Resuming after being loaded. |
| case Computer.State.Resuming if System.currentTimeMillis() >= sleepUntil => { |
| verifyComponents() |
| state.pop() |
| switchTo(state.top) // Trigger execution if necessary. |
| } |
| // Computer is rebooting. |
| case Computer.State.Restarting => { |
| close() |
| tmp.foreach(_.node.remove()) // To force deleting contents. |
| node.sendToReachable("computer.stopped") |
| start() |
| } |
| // Resume from pauses based on sleep or signal underflow. |
| case Computer.State.Sleeping if lastUpdate >= sleepUntil || !signals.isEmpty => { |
| switchTo(Computer.State.Yielded) |
| } |
| // Resume in case we paused because the game was paused. |
| case Computer.State.Paused => { |
| state.pop() |
| switchTo(state.top) // Trigger execution if necessary. |
| } |
| // Perform a synchronized call (message sending). |
| case Computer.State.SynchronizedCall => { |
| assert(future.isEmpty) |
| // 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 |
| // make this synchronized call fail due the limit still being maxed. |
| callCounts.clear() |
| // We switch into running state, since we'll behave as though the call |
| // were performed from our executor thread. |
| switchTo(Computer.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) |
| // Nothing should have been able to trigger a future. |
| assert(future.isEmpty) |
| // If the call lead to stop() being called we stop right now, |
| // otherwise we return the result to our executor. |
| if (state.top != Computer.State.Stopping) { |
| assert(state.top == Computer.State.Running) |
| switchTo(Computer.State.SynchronizedReturn) |
| } |
| } 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) |
| crash("protocol error") |
| } |
| } |
| case _ => // Nothing special to do, just avoid match errors. |
| }) |
| |
| // Finally check if we should stop the computer. We cannot lock the state |
| // because we may have to wait for the executor thread to finish, which |
| // might turn into a deadlock depending on where it currently is. |
| state.synchronized(state.top) match { |
| // Computer is shutting down. |
| case Computer.State.Stopping => this.synchronized(state.synchronized { |
| assert(future.isEmpty) |
| close() |
| rom.foreach(_.node.remove()) |
| tmp.foreach(_.node.remove()) |
| node.sendToReachable("computer.stopped") |
| }) |
| case _ => |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def onConnect(node: Node) { |
| if (node == this.node) { |
| rom.foreach(rom => node.connect(rom.node)) |
| tmp.foreach(tmp => node.connect(tmp.node)) |
| } |
| else { |
| node match { |
| case component: Component => addComponent(component) |
| case _ => |
| } |
| } |
| owner.onConnect(node) |
| } |
| |
| def onDisconnect(node: Node) { |
| if (node == this.node) { |
| stop() |
| rom.foreach(_.node.remove()) |
| tmp.foreach(_.node.remove()) |
| } |
| else { |
| node match { |
| case component: Component => removeComponent(component) |
| case _ => |
| } |
| } |
| owner.onDisconnect(node) |
| } |
| |
| def onMessage(message: Message) { |
| message.data match { |
| case Array(name: String, args@_*) if message.name == "computer.signal" => |
| signal(name, Seq(message.source.address) ++ args: _*) |
| case Array(player: EntityPlayer, name: String, args@_*) if message.name == "computer.checked_signal" => |
| if (isUser(player.getCommandSenderName)) |
| signal(name, Seq(message.source.address) ++ args: _*) |
| case _ => |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def addComponent(component: Component) { |
| if (!components.contains(component.address)) { |
| addedComponents += component |
| } |
| } |
| |
| def removeComponent(component: Component) { |
| if (components.contains(component.address)) { |
| components -= component.address |
| signal("component_removed", component.address, component.name) |
| } |
| addedComponents -= component |
| } |
| |
| private def processAddedComponents() { |
| for (component <- addedComponents) { |
| if (component.canBeSeenFrom(node)) { |
| 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) |
| signal("component_added", component.address, component.name) |
| } |
| } |
| addedComponents.clear() |
| } |
| |
| private def verifyComponents() { |
| val invalid = mutable.Set.empty[String] |
| for ((address, name) <- components) { |
| if (node.network.node(address) == null) { |
| OpenComputers.log.warning("A component of type '" + name + |
| "' disappeared! This usually means that it didn't save its node.") |
| signal("component_removed", address, name) |
| invalid += address |
| } |
| } |
| for (address <- invalid) { |
| components -= address |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def load(nbt: NBTTagCompound) = this.synchronized { |
| assert(state.top == Computer.State.Stopped) |
| assert(future.isEmpty) |
| |
| val computerNbt = nbt.getCompoundTag(Config.namespace + "computer") |
| |
| state.clear() |
| val stateNbt = computerNbt.getTagList("state") |
| (0 until stateNbt.tagCount). |
| map(stateNbt.tagAt). |
| map(_.asInstanceOf[NBTTagInt]). |
| foreach(s => state.push(Computer.State(s.data))) |
| |
| val usersNbt = computerNbt.getTagList("users") |
| (0 until usersNbt.tagCount). |
| map(usersNbt.tagAt). |
| map(_.asInstanceOf[NBTTagString]). |
| foreach(u => users += u.data) |
| |
| if (state.top != Computer.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(computerNbt.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(Computer.State.SynchronizedCall) || state.contains(Computer.State.SynchronizedReturn)) { |
| unpersist(computerNbt.getByteArray("stack")) |
| if (!(if (state.contains(Computer.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.") |
| } |
| } |
| |
| val componentsNbt = computerNbt.getTagList("components") |
| components ++= (0 until componentsNbt.tagCount). |
| map(componentsNbt.tagAt). |
| map(_.asInstanceOf[NBTTagCompound]). |
| map(c => c.getString("address") -> c.getString("name")) |
| |
| val signalsNbt = computerNbt.getTagList("signals") |
| signals ++= (0 until signalsNbt.tagCount). |
| map(signalsNbt.tagAt). |
| map(_.asInstanceOf[NBTTagCompound]). |
| map(signalNbt => { |
| val argsNbt = signalNbt.getCompoundTag("args") |
| val argsLength = argsNbt.getInteger("length") |
| new Computer.Signal(signalNbt.getString("name"), |
| (0 until argsLength).map("arg" + _).map(argsNbt.getTag).map { |
| case tag: NBTTagByte if tag.data == -1 => Unit |
| case tag: NBTTagByte => tag.data == 1 |
| case tag: NBTTagDouble => tag.data |
| case tag: NBTTagString => tag.data |
| case tag: NBTTagByteArray => tag.byteArray |
| case _ => Unit |
| }.toArray) |
| }) |
| |
| rom.foreach(rom => { |
| val romNbt = computerNbt.getCompoundTag("rom") |
| rom.node.load(romNbt) |
| rom.load(romNbt) |
| }) |
| tmp.foreach(tmp => { |
| val tmpNbt = computerNbt.getCompoundTag("tmp") |
| tmp.node.load(tmpNbt) |
| tmp.load(tmpNbt) |
| }) |
| kernelMemory = computerNbt.getInteger("kernelMemory") |
| timeStarted = computerNbt.getLong("timeStarted") |
| cpuTime = computerNbt.getLong("cpuTime") |
| if (computerNbt.hasKey("message")) { |
| message = Some(computerNbt.getString("message")) |
| } |
| |
| // Limit memory again. |
| recomputeMemory() |
| |
| // Delay execution for a second to allow the world around us to settle. |
| sleepUntil = System.currentTimeMillis() + 1000 * Config.startupDelay |
| if (state.top != Computer.State.Resuming) // Maybe saved while resuming? |
| state.push(Computer.State.Resuming) |
| } catch { |
| case e: LuaRuntimeException => { |
| OpenComputers.log.warning("Could not unpersist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) |
| state.push(Computer.State.Stopping) |
| } |
| } |
| } |
| else close() // Clean up in case we got a weird state stack. |
| } |
| |
| def save(nbt: NBTTagCompound): Unit = this.synchronized { |
| assert(state.top != Computer.State.Running) // Lock on 'this' should guarantee this. |
| assert(state.top != Computer.State.Stopping) // Only set while executor is running. |
| |
| // Make sure the component list is up-to-date. |
| processAddedComponents() |
| |
| val computerNbt = new NBTTagCompound() |
| |
| val stateNbt = new NBTTagList() |
| for (state <- state) { |
| stateNbt.appendTag(new NBTTagInt(null, state.id)) |
| } |
| computerNbt.setTag("state", stateNbt) |
| |
| val usersNbt = new NBTTagList() |
| for (user <- users) { |
| usersNbt.appendTag(new NBTTagString(null, user)) |
| } |
| computerNbt.setTag("users", usersNbt) |
| |
| if (state.top != Computer.State.Stopped) { |
| // 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)) |
| computerNbt.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(Computer.State.SynchronizedCall) || state.contains(Computer.State.SynchronizedReturn)) { |
| assert(if (state.contains(Computer.State.SynchronizedCall)) lua.isFunction(2) else lua.isTable(2)) |
| computerNbt.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) |
| } |
| computerNbt.setTag("components", componentsNbt) |
| |
| val signalsNbt = new NBTTagList() |
| for (s <- signals.iterator) { |
| val signalNbt = new NBTTagCompound() |
| signalNbt.setString("name", s.name) |
| val args = new NBTTagCompound() |
| 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) |
| } |
| signalNbt.setCompoundTag("args", args) |
| signalsNbt.appendTag(signalNbt) |
| } |
| computerNbt.setTag("signals", signalsNbt) |
| |
| val romNbt = new NBTTagCompound() |
| rom.foreach(rom => { |
| rom.save(romNbt) |
| rom.node.save(romNbt) |
| }) |
| computerNbt.setCompoundTag("rom", romNbt) |
| |
| val tmpNbt = new NBTTagCompound() |
| tmp.foreach(tmp => { |
| tmp.save(tmpNbt) |
| tmp.node.save(tmpNbt) |
| }) |
| computerNbt.setCompoundTag("tmp", tmpNbt) |
| |
| computerNbt.setInteger("kernelMemory", kernelMemory) |
| computerNbt.setLong("timeStarted", timeStarted) |
| computerNbt.setLong("cpuTime", cpuTime) |
| message.foreach(computerNbt.setString("message", _)) |
| } catch { |
| case e: LuaRuntimeException => { |
| OpenComputers.log.warning("Could not persist computer.\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat ")) |
| computerNbt.setInteger("state", Computer.State.Stopped.id) |
| } |
| } |
| |
| // Limit memory again. |
| recomputeMemory() |
| } |
| |
| nbt.setCompoundTag(Config.namespace + "computer", computerNbt) |
| } |
| |
| 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 = { |
| // Utility functions for varargs callbacks. |
| def parseArgument(lua: LuaState, index: Int): AnyRef = lua.`type`(index) match { |
| case LuaType.BOOLEAN => Boolean.box(lua.toBoolean(index)) |
| case LuaType.NUMBER => Double.box(lua.toNumber(index)) |
| case LuaType.STRING => lua.toByteArray(index) |
| case _ => Unit |
| } |
| |
| def parseArguments(lua: LuaState, start: Int) = |
| for (index <- start to lua.getTop) yield parseArgument(lua, index) |
| |
| def pushList(value: Iterator[(Any, Int)]) { |
| lua.newTable() |
| var count = 0 |
| value.foreach { |
| case (x, index) => { |
| x match { |
| case (entry: ScalaNumber) => |
| pushResult(lua, entry.underlying()) |
| case (entry) => |
| pushResult(lua, entry.asInstanceOf[AnyRef]) |
| } |
| lua.rawSet(-2, index + 1) |
| count = count + 1 |
| } |
| } |
| lua.pushString("n") |
| lua.pushInteger(count) |
| lua.rawSet(-3) |
| } |
| |
| def pushResult(lua: LuaState, value: AnyRef): Unit = value match { |
| case null | Unit | _: BoxedUnit => lua.pushNil() |
| case value: java.lang.Boolean => lua.pushBoolean(value.booleanValue) |
| case value: java.lang.Byte => lua.pushNumber(value.byteValue) |
| case value: java.lang.Character => lua.pushString(String.valueOf(value)) |
| case value: java.lang.Short => lua.pushNumber(value.shortValue) |
| case value: java.lang.Integer => lua.pushNumber(value.intValue) |
| case value: java.lang.Long => lua.pushNumber(value.longValue) |
| case value: java.lang.Float => lua.pushNumber(value.floatValue) |
| case value: java.lang.Double => lua.pushNumber(value.doubleValue) |
| case value: java.lang.String => lua.pushString(value) |
| case value: Array[Byte] => lua.pushByteArray(value) |
| case value: Array[_] => pushList(value.zipWithIndex.iterator) |
| case value: Product => pushList(value.productIterator.zipWithIndex) |
| case value: Seq[_] => pushList(value.zipWithIndex.iterator) |
| // TODO maps? |
| case _ => |
| OpenComputers.log.warning("A component callback tried to return an unsupported value of type " + value.getClass.getName + ".") |
| lua.pushNil() |
| } |
| |
| // 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 |
| return false |
| case Some(value) => lua = value |
| } |
| |
| // 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. |
| if (node.network != null) { |
| rom.foreach(rom => node.connect(rom.node)) |
| tmp.foreach(tmp => node.connect(tmp.node)) |
| } |
| |
| try { |
| // Push a couple of functions that override original Lua API functions or |
| // that add new functionality to it. |
| |
| // 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") |
| |
| // 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 os.uptime() to return real seconds, though, we'll divide it |
| // accordingly. |
| lua.pushNumber((worldTime - timeStarted) / 20.0) |
| 1 |
| }) |
| lua.setField(-2, "uptime") |
| |
| // Allow the system to read how much memory it uses and has available. |
| lua.pushScalaFunction(lua => { |
| lua.pushInteger(lua.getTotalMemory - kernelMemory) |
| 1 |
| }) |
| lua.setField(-2, "totalMemory") |
| |
| 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)) |
| 1 |
| }) |
| lua.setField(-2, "freeMemory") |
| |
| lua.pushScalaFunction(lua => { |
| lua.pushBoolean(signal(lua.checkString(1), parseArguments(lua, 2): _*)) |
| 1 |
| }) |
| lua.setField(-2, "pushSignal") |
| |
| // 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") |
| |
| // And it's 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 >= Config.maxUsers) |
| throw new Exception("too many users") |
| |
| val name = lua.checkString(1) |
| |
| if (users.contains(name)) |
| throw new Exception("user exists") |
| if (name.length > Config.maxUsernameLength) |
| throw new Exception("username too long") |
| if (!MinecraftServer.getServer.getConfigurationManager.getAllUsernames.contains(name)) |
| throw new Exception("player must be online") |
| |
| users += name |
| 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) |
| lua.pushBoolean(users.remove(name)) |
| 1 |
| }) |
| lua.setField(-2, "removeUser") |
| |
| // Pop the os table. |
| lua.pop(1) |
| |
| // 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") |
| |
| // How long programs may run without yielding before we stop them. |
| lua.pushNumber(Config.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) => |
| 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 = parseArguments(lua, 3) |
| try { |
| (Option(node.network.node(address)) match { |
| case Some(component: server.network.Component) if component.canBeSeenFrom(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(pushResult(lua, _)) |
| 1 + results.length |
| case _ => |
| lua.pushBoolean(true) |
| 1 |
| } |
| } catch { |
| 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) |
| 3 |
| case _: ArrayIndexOutOfBoundsException => |
| 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 _: Throwable => |
| lua.pushBoolean(true) |
| lua.pushNil() |
| lua.pushString("unknown error") |
| 3 |
| } |
| }) |
| lua.setField(-2, "invoke") |
| |
| lua.setGlobal("component") |
| |
| initPerms() |
| |
| lua.load(classOf[Computer].getResourceAsStream(Config.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 |
| } |
| catch { |
| case ex: Throwable => { |
| OpenComputers.log.log(Level.WARNING, "Failed initializing computer.", ex) |
| close() |
| } |
| } |
| 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.top != Computer.State.Stopped) { |
| state.clear() |
| state.push(Computer.State.Stopped) |
| lua.setTotalMemory(Integer.MAX_VALUE) |
| lua.close() |
| lua = null |
| kernelMemory = 0 |
| signals.clear() |
| timeStarted = 0 |
| cpuTime = 0 |
| cpuStart = 0 |
| future = None |
| sleepUntil = 0 |
| |
| // Mark state change in owner, to send it to clients. |
| owner.markAsChanged() |
| }) |
| |
| // ----------------------------------------------------------------------- // |
| |
| private def switchTo(value: Computer.State.Value) = { |
| val result = state.pop() |
| state.push(value) |
| if (value == Computer.State.Yielded || value == Computer.State.SynchronizedReturn) { |
| assert(future.isEmpty) |
| sleepUntil = 0 |
| future = Some(Computer.Executor.pool.submit(this)) |
| } |
| result |
| } |
| |
| // This is a really high level lock that we only use for saving and loading. |
| override def run(): Unit = this.synchronized { |
| future = None |
| |
| val enterState = state.synchronized { |
| if (state.top == Computer.State.Stopped || |
| state.top == Computer.State.Stopping || |
| state.top == Computer.State.Paused) { |
| return |
| } |
| // See if the game appears to be paused, in which case we also pause. |
| if (System.currentTimeMillis - lastUpdate > 100) { |
| state.push(Computer.State.Paused) |
| return |
| } |
| switchTo(Computer.State.Running) |
| } |
| |
| try { |
| // The kernel thread will always be at stack index one. |
| assert(lua.isThread(1)) |
| |
| // 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 Computer.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 Computer.State.Yielded => |
| if (kernelMemory == 0) { |
| // We're doing the initialization run. |
| lua.pop(lua.resume(1, 0)) |
| // 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 = ((lua.getTotalMemory - lua.getFreeMemory) + Config.baseMemory) max 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 { |
| case Unit => lua.pushNil() |
| case arg: Boolean => lua.pushBoolean(arg) |
| case arg: Double => lua.pushNumber(arg) |
| case arg: String => lua.pushString(arg) |
| case arg: Array[Byte] => lua.pushByteArray(arg) |
| } |
| lua.resume(1, 1 + signal.args.length) |
| case _ => |
| lua.resume(1, 0) |
| }, System.nanoTime() - cpuStart) |
| case s => throw new Exception("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) { |
| assert(isRunning) |
| // Intermediate state in some cases. Satisfies the assert in execute(). |
| future = None |
| // Check if someone called stop() in the meantime. |
| if (state.top != Computer.State.Stopping) { |
| // 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(Computer.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(Computer.State.Restarting) |
| else switchTo(Computer.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) * 1000 |
| else Double.PositiveInfinity |
| lua.pop(results) |
| signals.synchronized { |
| // Immediately check for signals to allow processing more than one |
| // signal per game tick. |
| if (signals.isEmpty) { |
| switchTo(Computer.State.Sleeping) |
| sleepUntil = System.currentTimeMillis + sleep |
| } else { |
| switchTo(Computer.State.Yielded) |
| } |
| } |
| } |
| |
| // State has inevitably changed, mark as changed to save again. |
| owner.markAsChanged() |
| } |
| } |
| // The kernel thread returned. If it threw we'd we 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") |
| } |
| }) |
| } |
| 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") |
| 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") |
| } |
| } |
| } |
| |
| object Computer { |
| |
| /** 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 { |
| /** The computer is not running right now and there is no Lua state. */ |
| val Stopped = Value("Stopped") |
| |
| /** Booting up, doing the first run to initialize the kernel and libs. */ |
| val Starting = Value("Starting") |
| |
| /** Resuming to run after being loaded, waiting for the world to settle. */ |
| val Resuming = Value("Resuming") |
| |
| /** Computer is currently rebooting. */ |
| val Restarting = Value("Restarting") |
| |
| /** The computer is currently shutting down. */ |
| val Stopping = Value("Stopping") |
| |
| /** The computer is running but yielding for a longer amount of time. */ |
| val Sleeping = Value("Sleeping") |
| |
| /** The computer is paused and waiting for the game to resume. */ |
| val Paused = Value("Paused") |
| |
| /** The computer executor is waiting for a synchronized call to be made. */ |
| val SynchronizedCall = Value("SynchronizedCall") |
| |
| /** The computer should resume with the result of a synchronized call. */ |
| val SynchronizedReturn = Value("SynchronizedReturn") |
| |
| /** The computer will resume as soon as possible. */ |
| val Yielded = Value("Yielded") |
| |
| /** The computer is up and running, executing Lua code. */ |
| val Running = Value("Running") |
| } |
| |
| /** Singleton for requesting executors that run our Lua states. */ |
| private object Executor { |
| val pool = Executors.newScheduledThreadPool(Config.threads, |
| new ThreadFactory() { |
| private val threadNumber = new AtomicInteger(1) |
| |
| private val group = System.getSecurityManager match { |
| case null => Thread.currentThread().getThreadGroup |
| case s => s.getThreadGroup |
| } |
| |
| def newThread(r: Runnable): Thread = { |
| val name = OpenComputers.getClass.getSimpleName + "-" + threadNumber.getAndIncrement |
| val thread = new Thread(group, r, name) |
| if (!thread.isDaemon) |
| thread.setDaemon(true) |
| if (thread.getPriority != Thread.MIN_PRIORITY) |
| thread.setPriority(Thread.MIN_PRIORITY) |
| thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler { |
| def uncaughtException(t: Thread, e: Throwable) { |
| OpenComputers.log.log(Level.WARNING, "Unhandled exception in worker thread.", e) |
| } |
| }) |
| thread |
| } |
| }) |
| } |
| |
| } |