| package li.cil.oc.server.computer |
| |
| import java.util.Calendar |
| import java.util.Locale |
| import java.util.concurrent._ |
| import java.util.concurrent.atomic.AtomicInteger |
| |
| import scala.Array.canBuildFrom |
| import scala.collection.JavaConversions._ |
| import scala.collection.mutable._ |
| import scala.io.Source |
| import scala.reflect.runtime.universe._ |
| import scala.util.Random |
| |
| import com.naef.jnlua._ |
| import li.cil.oc.Config |
| import li.cil.oc.api._ |
| import li.cil.oc.api.IComputerContext |
| import li.cil.oc.common.computer.IComputer |
| import li.cil.oc.server.components.IComponent |
| import net.minecraft.nbt._ |
| |
| /** |
| * Wrapper class for Lua states set up to behave like a pseudo-OS. |
| * |
| * This class takes care of the following: |
| * - Creating a new Lua state when started from a previously stopped state. |
| * - Updating the Lua state in a parallel thread so as not to block the game. |
| * - Managing a list of installed components and their drivers. |
| * - Synchronizing calls from the computer thread to drivers. |
| * - Saving the internal state of the computer across chunk saves/loads. |
| * - Closing the Lua state when stopping a previously running computer. |
| * |
| * See {@see Driver} to read more about component drivers and how they interact |
| * with computers - and through them the components they interface. |
| */ |
| class Computer(val owner: IComputerEnvironment) extends IComputerContext with IComputer with Runnable { |
| // ----------------------------------------------------------------------- // |
| // General |
| // ----------------------------------------------------------------------- // |
| |
| /** |
| * This is the list of components currently attached to the computer. It is |
| * updated whenever a component is placed into the computer or removed from |
| * it, as well as when a block component is placed next to it or removed. |
| * |
| * On the Lua side we only refer to components by an ID, which is the key. |
| * |
| * Since the Lua program may query a component's type from its own thread we |
| * have to synchronize access to the component map. Note that there's a |
| * chance the Lua programs may see a component before its install signal has |
| * been processed, but I can't think of a scenario where this could become |
| * a real problem, plus this can happen anyway if the signal queue is long. |
| */ |
| private val components = new HashMap[Int, String] with SynchronizedMap[Int, String] |
| |
| /** |
| * The current execution state of the computer. This is used to track how to |
| * resume the computers main thread, if at all, and whether to accept new |
| * signals or not. |
| */ |
| private var state = State.Stopped |
| |
| /** The internal Lua state. Only set while the computer is running. */ |
| private[computer] var lua: LuaState = null |
| |
| /** |
| * The base memory consumption of the kernel. Used to permit a fixed base |
| * memory for userland even if the amount of memory the kernel uses changes |
| * over time (i.e. with future releases of the mod). This is set when |
| * starting up the computer. |
| */ |
| private var kernelMemory = 0 |
| |
| /** |
| * The queue of signals the Lua state should process. Signals are queued from |
| * the Java side and processed one by one in the Lua VM. They are the only |
| * means to communicate actively with the computer (passively only drivers |
| * can interact with the computer by providing API functions). |
| */ |
| private val signals = new LinkedBlockingQueue[Signal](100) |
| |
| // ----------------------------------------------------------------------- // |
| |
| /** |
| * The time (world time) when the computer was started. This is used for our |
| * custom implementation of os.clock(), which returns the amount of the time |
| * the computer has been running. |
| */ |
| private var timeStarted = 0.0 |
| |
| /** |
| * The last time (system time) the update function was called by the server |
| * thread. We use this to detect whether the game was paused, to also pause |
| * the executor thread for our Lua state. |
| */ |
| private var lastUpdate = 0L |
| |
| /** |
| * The current world time. This is used for our custom implementation of |
| * os.time(). This is updated by the server thread and read by the computer |
| * thread, to avoid computer threads directly accessing the world state. |
| */ |
| private var worldTime = 0L |
| |
| // ----------------------------------------------------------------------- // |
| |
| /** |
| * This is used to keep track of the current executor of the Lua state, for |
| * example to wait for the computer to finish running a task. |
| */ |
| private var future: Option[Future[_]] = None |
| |
| /** This is used to synchronize access to the state field. */ |
| private val stateMonitor = new Object() |
| |
| /** This is used to synchronize while saving, so we don't stop while we do. */ |
| private val saveMonitor = new Object() |
| |
| // ----------------------------------------------------------------------- // |
| // IComputerContext |
| // ----------------------------------------------------------------------- // |
| |
| def world = owner.world |
| |
| def signal(name: String, args: Any*) = { |
| args.foreach { |
| case null | _: Byte | _: Char | _: Short | _: Int | _: Long | _: Float | _: Double | _: String => Unit |
| case _ => throw new IllegalArgumentException() |
| } |
| stateMonitor.synchronized(state match { |
| // We don't push new signals when stopped or shutting down. |
| case State.Stopped | State.Stopping => false |
| // Currently sleeping. Cancel that and start immediately. |
| case State.Sleeping => |
| future.get.cancel(true) |
| state = State.Suspended |
| signals.offer(new Signal(name, args.toArray)) |
| future = Some(Executor.pool.submit(this)) |
| true |
| // Running or in driver call or only a short yield, just push the signal. |
| case _ => |
| signals.offer(new Signal(name, args.toArray)) |
| true |
| }) |
| } |
| |
| def component[T](id: Int) = owner.component(id) match { |
| case None => throw new IllegalArgumentException("no such component") |
| case Some(component) => component.asInstanceOf[T] |
| } |
| |
| // ----------------------------------------------------------------------- // |
| // IComputer |
| // ----------------------------------------------------------------------- // |
| |
| def start() = stateMonitor.synchronized( |
| state == State.Stopped && init() && { |
| state = State.Suspended |
| |
| // Mark state change in owner, to send it to clients. |
| owner.markAsChanged() |
| |
| // Inject a dummy signal so that real ones don't get swallowed. This way |
| // we can just ignore the parameters the first time the kernel is run |
| // and all actual signals will be read using coroutine.yield(). |
| signal("dummy") |
| |
| // Initialize any installed components. |
| for (id <- components.keys) |
| owner.driver(id).get.instance.onInstall(this, owner.component(id).get) |
| |
| future = Some(Executor.pool.submit(this)) |
| true |
| }) |
| |
| def stop() = saveMonitor.synchronized(stateMonitor.synchronized { |
| if (state != State.Stopped) { |
| if (state != State.Running) { |
| // If the computer is not currently running we can simply close it, |
| // and cancel any pending future - which may already be running and |
| // waiting for the stateMonitor, so we do a hard abort. |
| future.foreach(_.cancel(true)) |
| close() |
| } |
| else { |
| // Otherwise we enter an intermediate state to ensure the executor |
| // truly stopped, before switching back to stopped to allow starting |
| // the computer again. The executor will check for this state and |
| // call close. |
| state = State.Stopping |
| } |
| true |
| } |
| else false |
| }) |
| |
| def isRunning = stateMonitor.synchronized(state != State.Stopped) |
| |
| def update() { |
| stateMonitor.synchronized(state match { |
| case State.Stopped | State.Stopping => return |
| case State.DriverCall => { |
| assert(lua.getTop() == 2) |
| assert(lua.isThread(1)) |
| assert(lua.isFunction(2)) |
| println("> drivercall") |
| lua.call(0, 1) |
| println("< drivercall") |
| assert(lua.isTable(2)) |
| state = State.DriverReturn |
| future = Some(Executor.pool.submit(this)) |
| } |
| case State.Paused | State.DriverReturnPaused => { |
| state = State.Suspended |
| future = Some(Executor.pool.submit(this)) |
| } |
| case _ => /* Nothing special to do. */ |
| }) |
| |
| // Remember when we started the computer for os.clock(). We do this in the |
| // update because only then can we be sure the world is available. |
| if (timeStarted == 0) |
| timeStarted = owner.world.getWorldInfo().getWorldTotalTime() |
| |
| // Update world time for computer threads. |
| worldTime = owner.world.getWorldInfo().getWorldTotalTime() |
| |
| // Update last time run to let our executor thread know it doesn't have to |
| // pause. |
| lastUpdate = System.currentTimeMillis |
| } |
| |
| // ----------------------------------------------------------------------- // |
| // Note: driver interaction is synchronized, so we don't have to lock here. |
| |
| def add(component: Any, driver: Driver) = { |
| val id = driver.instance.id(component) |
| if (components.contains(id)) false |
| else { |
| components += id -> driver.instance.componentName |
| driver.instance.onInstall(this, component) |
| signal("component_install", id) |
| true |
| } |
| } |
| |
| def remove(id: Int) = |
| components.remove(id) match { |
| case None => false |
| case Some(_) => { |
| // TODO won't work for blocks, because we won't know the old one at |
| // this point, since we only get the info that *some* neighbor block |
| // changed, and not which, and much less what it previously was... |
| //owner.driver(id).get.instance.onUninstall(this, owner.component(id)) |
| signal("component_uninstall", id) |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def readFromNBT(nbt: NBTTagCompound): Unit = |
| saveMonitor.synchronized(this.synchronized { |
| // Clear out what we currently have, if anything. |
| stateMonitor.synchronized { |
| assert(state != State.Running) // Lock on 'this' should guarantee this. |
| stop() |
| } |
| |
| components.clear() |
| val componentList = nbt.getTagList("components") |
| for (i <- 0 until componentList.tagCount) { |
| val component = componentList.tagAt(i).asInstanceOf[NBTTagCompound] |
| val id = component.getInteger("id") |
| val componentName = component.getString("componentName") |
| components += id -> componentName |
| } |
| |
| state = State(nbt.getInteger("state")) |
| |
| if (state != State.Stopped && init()) { |
| // Unlimit memory use while unpersisting. |
| val memory = lua.getTotalMemory() |
| lua.setTotalMemory(Integer.MAX_VALUE) |
| try { |
| // Try unpersisting Lua, because that's what all of the rest depends |
| // on. First, clear the stack, meaning the current kernel. |
| lua.setTop(0) |
| |
| if (!unpersist(nbt.getByteArray("kernel")) || !lua.isThread(1)) { |
| // This shouldn't really happen, but there's a chance it does if |
| // the save was corrupt (maybe someone modified the Lua files). |
| throw new IllegalStateException("Could not restore kernel.") |
| } |
| if (state == State.DriverCall || state == State.DriverReturn) { |
| if (!unpersist(nbt.getByteArray("stack")) || |
| (state == State.DriverCall && !lua.isFunction(2)) || |
| (state == State.DriverReturn && !lua.isTable(2))) { |
| // Same as with the above, should not really happen normally, but |
| // could for the same reasons. |
| throw new IllegalStateException("Could not restore driver call.") |
| } |
| } |
| |
| assert(signals.size() == 0) |
| val signalsTag = nbt.getTagList("signals") |
| signals.addAll((0 until signalsTag.tagCount()). |
| map(signalsTag.tagAt(_).asInstanceOf[NBTTagCompound]). |
| map(signal => { |
| val argsTag = signal.getCompoundTag("args") |
| val argsLength = argsTag.getInteger("length") |
| new Signal(signal.getString("name"), |
| (0 until argsLength).map("arg" + _).map(argsTag.getTag(_)).map { |
| case tag: NBTTagByte => tag.data |
| case tag: NBTTagShort => tag.data |
| case tag: NBTTagInt => tag.data |
| case tag: NBTTagLong => tag.data |
| case tag: NBTTagFloat => tag.data |
| case tag: NBTTagDouble => tag.data |
| case tag: NBTTagString => tag.data |
| }.toArray) |
| })) |
| |
| timeStarted = nbt.getDouble("timeStarted") |
| |
| // Start running our worker thread. |
| assert(!future.isDefined) |
| future = Some(Executor.pool.submit(this)) |
| } |
| catch { |
| case t: Throwable => { |
| t.printStackTrace() |
| // TODO display error in-game on monitor or something |
| //signal("crash", "memory corruption") |
| close() |
| } |
| } |
| finally if (lua != null) { |
| // Clean up some after we're done and limit memory again. |
| lua.gc(LuaState.GcAction.COLLECT, 0) |
| lua.setTotalMemory(memory) |
| } |
| } |
| }) |
| |
| def writeToNBT(nbt: NBTTagCompound): Unit = |
| saveMonitor.synchronized(this.synchronized { |
| stateMonitor.synchronized { |
| assert(state != State.Running) // Lock on 'this' should guarantee this. |
| assert(state != State.Stopping) // Only set while executor is running. |
| } |
| |
| val componentList = new NBTTagList |
| for ((id, componentName) <- components) { |
| val component = new NBTTagCompound |
| component.setInteger("id", id) |
| component.setString("componentName", componentName) |
| componentList.appendTag(component) |
| } |
| nbt.setTag("components", componentList) |
| |
| nbt.setInteger("state", state.id) |
| if (state == State.Stopped) { |
| return |
| } |
| |
| // Unlimit memory while persisting. |
| val memory = lua.getTotalMemory() |
| lua.setTotalMemory(Integer.MAX_VALUE) |
| try { |
| // Try persisting Lua, because that's what all of the rest depends on. |
| // While in a driver call we have one object on the global stack: either |
| // the function to call the driver with, or the result of the call. |
| if (state == State.DriverCall || state == State.DriverReturn) { |
| assert( |
| if (state == State.DriverCall) lua.`type`(2) == LuaType.FUNCTION |
| else lua.`type`(2) == LuaType.TABLE) |
| nbt.setByteArray("stack", persist()) |
| } |
| // Save the kernel state (which is always at stack index one). |
| assert(lua.`type`(1) == LuaType.THREAD) |
| nbt.setByteArray("kernel", persist()) |
| |
| val list = new NBTTagList |
| for (s <- signals.iterator) { |
| val signal = new NBTTagCompound |
| signal.setString("name", s.name) |
| // TODO Test with NBTTagList, but supposedly it only allows entries |
| // with the same type, so I went with this for now... |
| val args = new NBTTagCompound |
| args.setInteger("length", s.args.length) |
| s.args.zipWithIndex.foreach { |
| case (arg: Byte, i) => args.setByte("arg" + i, arg) |
| case (arg: Short, i) => args.setShort("arg" + i, arg) |
| case (arg: Int, i) => args.setInteger("arg" + i, arg) |
| case (arg: Long, i) => args.setLong("arg" + i, arg) |
| case (arg: Float, i) => args.setFloat("arg" + i, arg) |
| case (arg: Double, i) => args.setDouble("arg" + i, arg) |
| case (arg: String, i) => args.setString("arg" + i, arg) |
| } |
| signal.setCompoundTag("args", args) |
| list.appendTag(signal) |
| } |
| nbt.setTag("signals", list) |
| |
| nbt.setDouble("timeStarted", timeStarted) |
| } |
| catch { |
| case t: Throwable => { |
| t.printStackTrace() |
| nbt.setInteger("state", State.Stopped.id) |
| } |
| } |
| finally { |
| // Clean up some after we're done and limit memory again. |
| lua.gc(LuaState.GcAction.COLLECT, 0) |
| lua.setTotalMemory(memory) |
| } |
| }) |
| |
| private def persist(): Array[Byte] = { |
| lua.getGlobal("persist") // ... obj persist? |
| if (lua.`type`(-1) == LuaType.FUNCTION) { // ... obj persist |
| lua.pushValue(-2) // ... obj persist obj |
| lua.call(1, 1) // ... obj str? |
| if (lua.`type`(-1) == LuaType.STRING) { // ... obj str |
| val result = lua.toByteArray(-1) |
| lua.pop(1) // ... obj |
| return result |
| } // ... obj :( |
| } // ... obj :( |
| lua.pop(1) // ... obj |
| return Array[Byte]() |
| } |
| |
| private def unpersist(value: Array[Byte]): Boolean = { |
| lua.getGlobal("unpersist") // ... unpersist? |
| if (lua.`type`(-1) == LuaType.FUNCTION) { // ... unpersist |
| lua.pushByteArray(value) // ... unpersist str |
| lua.call(1, 1) // ... obj |
| return true |
| } // ... :( |
| return false |
| } |
| |
| // ----------------------------------------------------------------------- // |
| // Internals |
| // ----------------------------------------------------------------------- // |
| |
| private 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; return false |
| case Some(state) => lua = state |
| } |
| |
| try { |
| // Push a couple of functions that override original Lua API functions or |
| // that add new functionality to it. |
| lua.newTable() |
| |
| // Set up driver information callbacks, i.e. to query for the presence of |
| // components with a given ID, and their type. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.pushBoolean(components.contains(lua.checkInteger(1))) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "exists") |
| |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| val id = lua.checkInteger(1) |
| components.get(id) match { |
| case None => lua.pushNil() |
| case Some(componentName) => lua.pushString(componentName) |
| } |
| return 1 |
| } |
| }) |
| lua.setField(-2, "type") |
| |
| // "Pop" the components table. |
| lua.setGlobal("component") |
| |
| // 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 running, instead of the native library... |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| // World time is in ticks, and each second has 20 ticks. Since we |
| // want os.clock() to return real seconds, though, we'll divide it |
| // accordingly. |
| lua.pushNumber((owner.world.getTotalWorldTime() - timeStarted) / 20.0) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "clock") |
| |
| // Return ingame time for os.time(). |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| // 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) / 1000[h] * 60[m] * 60[s] * 1000[ms] |
| lua.pushNumber((worldTime + 6000) * 60 * 60) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "time") |
| |
| // Date-time formatting using Java's formatting capabilities. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| val calendar = Calendar.getInstance(Locale.ENGLISH); |
| calendar.setTimeInMillis(lua.checkInteger(1)) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "date") |
| |
| // Custom os.difftime(). For most Lua implementations this would be the |
| // same anyway, but just to be on the safe side. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| val t2 = lua.checkNumber(1) |
| val t1 = lua.checkNumber(2) |
| lua.pushNumber(t2 - t1) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "difftime") |
| |
| // Allow getting the real world time via os.realTime() for timeouts. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.pushNumber(System.currentTimeMillis() / 1000.0) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "realTime") |
| |
| // Allow the system to read how much memory it uses and has available. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.pushInteger(lua.getTotalMemory() - kernelMemory) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "totalMemory") |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.pushInteger(lua.getFreeMemory()) |
| return 1 |
| } |
| }) |
| lua.setField(-2, "freeMemory") |
| |
| // Pop the os table. |
| lua.pop(1) |
| |
| lua.getGlobal("math") |
| |
| // We give each Lua state it's own randomizer, since otherwise they'd |
| // use the good old rand() from C. Which can be terrible, and isn't |
| // necessarily thread-safe. |
| val random = new Random |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.getTop() match { |
| case 0 => lua.pushNumber(random.nextDouble) |
| case 1 => { |
| val u = lua.checkInteger(1) |
| lua.checkArg(1, 1 < u, "interval is empty") |
| lua.pushInteger(1 + random.nextInt(u)) |
| } |
| case 2 => { |
| val l = lua.checkInteger(1) |
| val u = lua.checkInteger(2) |
| lua.checkArg(1, l < u, "interval is empty") |
| lua.pushInteger(l + random.nextInt(u - l)) |
| } |
| case _ => throw new IllegalArgumentException("wrong number of arguments") |
| } |
| return 1 |
| } |
| }) |
| lua.setField(-2, "random") |
| |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| val seed = lua.checkInteger(1) |
| random.setSeed(seed) |
| return 0 |
| } |
| }) |
| lua.setField(-2, "randomseed") |
| |
| // Pop the math table. |
| lua.pop(1) |
| |
| // Until we get to ingame screens we log to Java's stdout. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| for (i <- 1 to lua.getTop()) { |
| lua.`type`(i) match { |
| case LuaType.NIL => print("nil") |
| case LuaType.BOOLEAN => print(lua.toBoolean(i)) |
| case LuaType.NUMBER => print(lua.toNumber(i)) |
| case LuaType.STRING => print(lua.toString(i)) |
| case LuaType.TABLE => print("table") |
| case LuaType.FUNCTION => print("function") |
| case LuaType.THREAD => print("thread") |
| case LuaType.LIGHTUSERDATA | LuaType.USERDATA => print("userdata") |
| } |
| } |
| println() |
| return 0 |
| } |
| }) |
| lua.setGlobal("print") |
| |
| // TODO Other overrides? |
| |
| // Install all driver callbacks into the state. This is done once in |
| // the beginning so that we can take the memory the callbacks use into |
| // account when computing the kernel's memory use, as well as for |
| // building a table of permanent values used when persisting/unpersisting |
| // the state. |
| lua.newTable() |
| lua.setGlobal("driver") |
| Drivers.installOn(this) |
| |
| // Run the boot script. This creates the global sandbox variable that is |
| // used as the environment for any processes the kernel spawns, adds a |
| // couple of library functions and sets up the permanent value tables as |
| // well as making the functions used for persisting/unpersisting |
| // available as globals. |
| lua.load(classOf[Computer].getResourceAsStream( |
| "/assets/opencomputers/lua/boot.lua"), "boot", "t") |
| lua.call(0, 0) |
| |
| // Load the basic kernel which takes care of handling signals by managing |
| // the list of active processes. Whatever functionality we can we |
| // implement in Lua, so we also implement most of the kernel's |
| // functionality in Lua. Why? Because like this it's automatically |
| // persisted for us without having to write more additional NBT stuff. |
| lua.load(classOf[Computer].getResourceAsStream( |
| "/assets/opencomputers/lua/kernel.lua"), "kernel", "t") |
| lua.newThread() // Leave it as the first value on the stack. |
| |
| // Loads the init script. This is run by the kernel as a separate |
| // coroutine to enforce timeouts and sandbox user scripts. It also |
| // provides some basic functionality to make working with signals easier. |
| lua.pushJavaFunction(new JavaFunction() { |
| def invoke(lua: LuaState): Int = { |
| lua.pushString(Source.fromInputStream(classOf[Computer]. |
| getResourceAsStream("/assets/opencomputers/lua/init.lua")).mkString) |
| return 1 |
| } |
| }) |
| lua.setGlobal("init") |
| |
| // Run the garbage collector to get rid of stuff left behind after the |
| // initialization phase to get a good estimate of the base memory usage |
| // the kernel has. We remember that size to grant user-space programs a |
| // fixed base amount of memory, 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() |
| lua.setTotalMemory(kernelMemory + 64 * 1024) |
| |
| // Clear any left-over signals from a previous run. |
| signals.clear() |
| |
| return true |
| } |
| catch { |
| case ex: Throwable => { |
| ex.printStackTrace() |
| close() |
| } |
| } |
| return false |
| } |
| |
| private def close(): Unit = stateMonitor.synchronized( |
| if (state != State.Stopped) { |
| state = State.Stopped |
| |
| // Shutdown any installed components. |
| for (id <- components.keys) |
| owner.driver(id).get.instance.onUninstall(this, owner.component(id).get) |
| |
| lua.setTotalMemory(Integer.MAX_VALUE); |
| lua.close() |
| lua = null |
| kernelMemory = 0 |
| signals.clear() |
| timeStarted = 0 |
| future = None |
| |
| // Mark state change in owner, to send it to clients. |
| owner.markAsChanged() |
| }) |
| |
| // This is a really high level lock that we only use for saving and loading. |
| def run(): Unit = this.synchronized { |
| println(" > executor enter") |
| |
| // See if the game appears to be paused, in which case we also pause. |
| if (System.currentTimeMillis - lastUpdate > 200) |
| stateMonitor.synchronized { |
| state = |
| if (state == State.DriverReturn) State.DriverReturnPaused |
| else State.Paused |
| future = None |
| println(" < executor leave") |
| return |
| } |
| |
| val driverReturn = stateMonitor.synchronized { |
| if (state == State.Stopped) return |
| val oldState = state |
| state = State.Running |
| oldState |
| } match { |
| case State.DriverReturn | State.DriverReturnPaused => true |
| case _ => false |
| } |
| |
| try { |
| // This is synchronized so that we don't run a Lua state while saving or |
| // loading the computer to or from an NBTTagCompound or other stuff |
| // corrupting our Lua state. |
| |
| // The kernel thread will always be at stack index one. |
| assert(lua.`type`(1) == LuaType.THREAD) |
| |
| // Resume the Lua state and remember the number of results we get. |
| val results = if (driverReturn) { |
| // If we were doing a driver call, continue where we left off. |
| assert(lua.getTop() == 2) |
| lua.resume(1, 1) |
| } |
| else signals.poll() match { |
| // No signal, just run any non-sleeping processes. |
| case null => lua.resume(1, 0) |
| |
| // Got a signal, inject it and call any handlers (if any). |
| case signal => { |
| lua.pushString(signal.name) |
| signal.args.foreach(lua.pushJavaObject) |
| lua.resume(1, 1 + signal.args.length) |
| } |
| } |
| |
| // State has inevitably changed, mark as changed to save again. |
| owner.markAsChanged() |
| |
| // Only queue for next execution step if the kernel is still alive. |
| if (lua.status(1) == LuaState.YIELD) { |
| // Lua state yielded normally, see what we have. |
| stateMonitor.synchronized { |
| if (state == State.Stopping) { |
| // Someone called stop() in the meantime. |
| close() |
| } |
| else if (results == 1 && lua.isNumber(2)) { |
| println("sleep") |
| // We got a number. This tells us how long we should wait before |
| // resuming the state again. |
| val sleep = (lua.toNumber(2) * 1000).toLong |
| lua.pop(results) |
| state = State.Sleeping |
| future = Some(Executor.pool. |
| schedule(this, sleep, TimeUnit.MILLISECONDS)) |
| } |
| else if (results == 1 && lua.isFunction(2)) { |
| println("drivercall") |
| // If we get one function it's a wrapper for a driver call. |
| state = State.DriverCall |
| future = None |
| } |
| else { |
| // Something else, just pop the results and try again. |
| lua.pop(results) |
| state = State.Suspended |
| future = Some(Executor.pool.submit(this)) |
| } |
| } |
| |
| println(" < executor leave") |
| |
| // Avoid getting to the closing part after the exception handling. |
| return |
| } |
| else if (lua.isBoolean(2) && !lua.toBoolean(2)) { |
| println(lua.toString(3)) |
| } |
| } |
| catch { |
| // The kernel should never throw. If it does, the computer crashed |
| // hard, so we just close the state. |
| // TODO Print something to an in-game screen, a la kernel panic. |
| case ex: LuaRuntimeException => ex.printLuaStackTrace() |
| case er: LuaMemoryAllocationException => { |
| // This is pretty likely to happen for non-upgraded computers. |
| // TODO Print an error message to an in-game screen. |
| println("Out of memory!") |
| er.printStackTrace() |
| } |
| // Top-level catch-anything, because otherwise those exceptions get |
| // gobbled up by the executor unless we call the future's get(). |
| case t: Throwable => t.printStackTrace() |
| } |
| |
| // If we come here there was an error or we stopped, kill off the state. |
| close() |
| |
| println(" < executor leave") |
| } |
| |
| /** Signals are messages sent to the Lua state 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") |
| |
| /** The computer is running but yielded for a moment. */ |
| val Suspended = Value("Suspended") |
| |
| /** The computer is running but yielding for a longer amount of time. */ |
| val Sleeping = Value("Sleeping") |
| |
| /** The computer is paused and waiting for the game to resume. */ |
| val Paused = Value("Paused") |
| |
| /** The computer is up and running, executing Lua code. */ |
| val Running = Value("Running") |
| |
| /** The computer is currently shutting down (waiting for executor). */ |
| val Stopping = Value("Stopping") |
| |
| /** The computer executor is waiting for a driver call to be made. */ |
| val DriverCall = Value("DriverCall") |
| |
| /** The computer should resume with the result of a driver call. */ |
| val DriverReturn = Value("DriverReturn") |
| |
| /** The computer is paused and waiting for the game to resume. */ |
| val DriverReturnPaused = Value("DriverReturnPaused") |
| } |
| } |
| |
| /** Singleton for requesting executors that run our Lua states. */ |
| private[computer] 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-" + 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) |
| return thread |
| } |
| }) |
| } |