added setting to allow logging of tracebacks for exceptions that occurred in Lua callbacks (disabled per default to avoid spamming the log with InvalidArgumentExceptions and the like); moved argument parsing and result pushing to extended lua state and added capability to push maps as tables, meaning callbacks can now return maps, too

This commit is contained in:
Florian Nücke 2014-01-06 17:19:04 +01:00
parent 9478907a58
commit 3b59b53d5c
5 changed files with 145 additions and 119 deletions

View File

@ -43,6 +43,7 @@ class Settings(config: Config) {
val maxUsers = config.getInt("computer.maxUsers") max 0 val maxUsers = config.getInt("computer.maxUsers") max 0
val maxUsernameLength = config.getInt("computer.maxUsernameLength") max 0 val maxUsernameLength = config.getInt("computer.maxUsernameLength") max 0
val allowBytecode = config.getBoolean("computer.allowBytecode") val allowBytecode = config.getBoolean("computer.allowBytecode")
val logLuaCallbackErrors = config.getBoolean("computer.logCallbackErrors")
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
// robot // robot

View File

@ -20,8 +20,6 @@ import scala.Array.canBuildFrom
import scala.Some import scala.Some
import scala.collection.convert.WrapAsScala._ import scala.collection.convert.WrapAsScala._
import scala.collection.mutable import scala.collection.mutable
import scala.math.ScalaNumber
import scala.runtime.BoxedUnit
class Computer(val owner: tileentity.Computer) extends ManagedComponent with Context with Runnable { class Computer(val owner: tileentity.Computer) extends ManagedComponent with Context with Runnable {
val node = api.Network.newNode(this, Visibility.Network). val node = api.Network.newNode(this, Visibility.Network).
@ -665,57 +663,6 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con
// ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- //
private def init(): Boolean = { 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 LuaType.TABLE => lua.toJavaObject(index, classOf[java.util.Map[_, _]])
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. // Reset error state.
message = None message = None
@ -862,7 +809,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con
lua.setField(-2, "totalMemory") lua.setField(-2, "totalMemory")
lua.pushScalaFunction(lua => { lua.pushScalaFunction(lua => {
lua.pushBoolean(signal(lua.checkString(1), parseArguments(lua, 2): _*)) lua.pushBoolean(signal(lua.checkString(1), lua.toSimpleJavaObjects(2): _*))
1 1
}) })
lua.setField(-2, "pushSignal") lua.setField(-2, "pushSignal")
@ -1029,7 +976,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con
lua.pushScalaFunction(lua => { lua.pushScalaFunction(lua => {
val address = lua.checkString(1) val address = lua.checkString(1)
val method = lua.checkString(2) val method = lua.checkString(2)
val args = parseArguments(lua, 3) val args = lua.toSimpleJavaObjects(3)
try { try {
(Option(node.network.node(address)) match { (Option(node.network.node(address)) match {
case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node =>
@ -1048,57 +995,68 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con
}) match { }) match {
case results: Array[_] => case results: Array[_] =>
lua.pushBoolean(true) lua.pushBoolean(true)
results.foreach(pushResult(lua, _)) results.foreach(result => lua.pushValue(result))
1 + results.length 1 + results.length
case _ => case _ =>
lua.pushBoolean(true) lua.pushBoolean(true)
1 1
} }
} catch { }
case _: LimitReachedException => catch {
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 e: Throwable => case e: Throwable =>
OpenComputers.log.log(Level.WARNING, "Unexpected error in Lua callback.", e) if (Settings.get.logLuaCallbackErrors) {
lua.pushBoolean(true) OpenComputers.log.log(Level.WARNING, "Exception in Lua callback.", e)
lua.pushNil() }
lua.pushString("unknown error") e match {
3 case _: LimitReachedException =>
0
case e: IllegalArgumentException if e.getMessage != null =>
lua.pushBoolean(false)
lua.pushString(e.getMessage)
2
case e: Throwable if e.getMessage != null =>
lua.pushBoolean(true)
lua.pushNil()
lua.pushString(e.getMessage)
if (true) {
lua.pushString(e.getStackTraceString)
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.setField(-2, "invoke")
@ -1293,19 +1251,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con
else (signals.synchronized(if (signals.isEmpty) None else Some(signals.dequeue())) match { else (signals.synchronized(if (signals.isEmpty) None else Some(signals.dequeue())) match {
case Some(signal) => case Some(signal) =>
lua.pushString(signal.name) lua.pushString(signal.name)
signal.args.foreach { signal.args.foreach(arg => lua.pushValue(arg))
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)
case arg: Map[String, String] =>
lua.newTable(0, arg.size)
for ((key, value) <- arg if key != null && value != null) {
lua.pushString(value)
lua.setField(-2, key)
}
}
lua.resume(1, 1 + signal.args.length) lua.resume(1, 1 + signal.args.length)
case _ => case _ =>
lua.resume(1, 0) lua.resume(1, 0)

View File

@ -3,7 +3,6 @@ package li.cil.oc.server.network
import cpw.mods.fml.common.FMLCommonHandler import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.relauncher.Side import cpw.mods.fml.relauncher.Side
import java.lang.reflect.{Method, InvocationTargetException} import java.lang.reflect.{Method, InvocationTargetException}
import java.util
import li.cil.oc.api import li.cil.oc.api
import li.cil.oc.api.network._ import li.cil.oc.api.network._
import li.cil.oc.server.component import li.cil.oc.server.component
@ -249,7 +248,7 @@ object Component {
def isTable(index: Int) = def isTable(index: Int) =
index >= 0 && index < count && (args(index) match { index >= 0 && index < count && (args(index) match {
case value: util.Map[_, _] => true case value: java.util.Map[_, _] => true
case value: Map[_, _] => true case value: Map[_, _] => true
case value: mutable.Map[_, _] => true case value: mutable.Map[_, _] => true
case _ => false case _ => false
@ -272,7 +271,7 @@ object Component {
case _: java.lang.Double => "double" case _: java.lang.Double => "double"
case _: java.lang.String => "string" case _: java.lang.String => "string"
case _: Array[Byte] => "string" case _: Array[Byte] => "string"
case value: util.Map[_, _] => "table" case value: java.util.Map[_, _] => "table"
case value: Map[_, _] => "table" case value: Map[_, _] => "table"
case value: mutable.Map[_, _] => "table" case value: mutable.Map[_, _] => "table"
case _ => value.getClass.getSimpleName case _ => value.getClass.getSimpleName

View File

@ -1,16 +1,87 @@
package li.cil.oc.util package li.cil.oc.util
import com.naef.jnlua.{JavaFunction, LuaState} import com.naef.jnlua.{LuaType, JavaFunction, LuaState}
import li.cil.oc.OpenComputers
import scala.collection.convert.WrapAsScala._
import scala.collection.mutable
import scala.language.implicitConversions import scala.language.implicitConversions
import scala.math.ScalaNumber
import scala.runtime.BoxedUnit
object ExtendedLuaState { object ExtendedLuaState {
implicit def extendLuaState(state: LuaState) = new ExtendedLuaState(state) implicit def extendLuaState(state: LuaState) = new ExtendedLuaState(state)
class ExtendedLuaState(val state: LuaState) { class ExtendedLuaState(val lua: LuaState) {
def pushScalaFunction(f: (LuaState) => Int) = state.pushJavaFunction(new JavaFunction { def pushScalaFunction(f: (LuaState) => Int) = lua.pushJavaFunction(new JavaFunction {
override def invoke(state: LuaState) = f(state) override def invoke(state: LuaState) = f(state)
}) })
def pushValue(value: Any) {
(value match {
case number: ScalaNumber => number.underlying
case reference: AnyRef => reference
case null => null
case primitive => primitive.asInstanceOf[AnyRef]
}) 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)
case value: java.util.Map[_, _] => pushTable(value.toMap)
case value: Map[_, _] => pushTable(value)
case value: mutable.Map[_, _] => pushTable(value.toMap)
case _ =>
OpenComputers.log.warning("Tried to push an unsupported value of type to Lua: " + value.getClass.getName + ".")
lua.pushNil()
}
}
def pushList(list: Iterator[(Any, Int)]) {
lua.newTable()
var count = 0
list.foreach {
case (value, index) =>
pushValue(value)
lua.rawSet(-2, index + 1)
count = count + 1
}
lua.pushString("n")
lua.pushInteger(count)
lua.rawSet(-3)
}
def pushTable(map: Map[_, _]) {
lua.newTable(0, map.size)
for ((key: AnyRef, value: AnyRef) <- map) {
if (key != null && key != Unit && !key.isInstanceOf[BoxedUnit]) {
pushValue(key)
pushValue(value)
lua.setTable(-3)
}
}
}
def toSimpleJavaObject(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 LuaType.TABLE => lua.toJavaObject(index, classOf[java.util.Map[_, _]])
case _ => null
}
def toSimpleJavaObjects(start: Int) =
for (index <- start to lua.getTop) yield toSimpleJavaObject(index)
} }
} }

View File

@ -150,6 +150,15 @@ opencomputers {
# list of registered users on the Lua side. # list of registered users on the Lua side.
# See also: `canComputersBeOwned`. # See also: `canComputersBeOwned`.
maxUsernameLength: 32 maxUsernameLength: 32
# This setting is meant for debugging errors that occur in Lua callbacks.
# Per default, if an error occurs and it has a message set, only the
# message is pushed back to Lua, and that's it. If you encounter weird
# errors or are developing an addon you'll want the stacktrace for those
# errors. Enabling this setting will log them to the game log. This is
# disabled per default to avoid spamming the log with inconsequentual
# exceptions such as IllegalArgumentExceptions and the like.
logCallbackErrors: false
} }
# Robot related settings, what they may do and general balancing. # Robot related settings, what they may do and general balancing.