blob: 651faf4769956addb4a99ab82f5ab71cef18ef4b [file] [log] [blame] [raw]
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
}
}