Cleaned up RMI save/get/create methods. Fixed issues waiting for RMI method invocation responses
This commit is contained in:
parent
47891cc448
commit
ccb1267bab
@ -55,6 +55,6 @@ open class CachedMethod(val method: Method, val methodIndex: Int, val methodClas
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "CachedMethod{" + method.name + ", methodClassId=" + methodClassId + ", methodIndex=" + methodIndex + '}'
|
return "CachedMethod{name:" + method.name + ", methodClassId=" + methodClassId + ", methodIndex=" + methodIndex + '}'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,12 +47,7 @@ interface RemoteObject {
|
|||||||
var responseTimeout: Int
|
var responseTimeout: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the ID of response for the last method invocation.
|
* Sets the behavior when invoking a remote method. DEFAULT is false.
|
||||||
*/
|
|
||||||
val lastResponseId: Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the behavior when invoking a remote method. Default is false.
|
|
||||||
*
|
*
|
||||||
* If true, the invoking thread will not wait for a response. The method will return immediately and the return value
|
* If true, the invoking thread will not wait for a response. The method will return immediately and the return value
|
||||||
* should be ignored.
|
* should be ignored.
|
||||||
@ -60,50 +55,33 @@ interface RemoteObject {
|
|||||||
* If false, the invoking thread will wait (if called via suspend, then it will use coroutines) for the remote method to return or
|
* If false, the invoking thread will wait (if called via suspend, then it will use coroutines) for the remote method to return or
|
||||||
* timeout.
|
* timeout.
|
||||||
*
|
*
|
||||||
* The return value or any thrown exception can later be retrieved with [RemoteObject.waitForLastResponse] or [RemoteObject.waitForResponse].
|
* If the return value or exception needs to be retrieved, then DO NOT set async, and change the response timeout
|
||||||
* The responses will be stored until retrieved, so each method call should have a matching retrieve.
|
|
||||||
*/
|
*/
|
||||||
var async: Boolean
|
var async: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permits calls to [Object.toString] to actually return the `toString()` method on the object.
|
* Permits calls to [Object.toString] to actually return the `toString()` method on the object.
|
||||||
*
|
*
|
||||||
* @param enableDetailedToString If false, calls to [Object.toString] will return "<proxy #id>" (where `id` is the remote object ID)
|
* @param enabled If false, calls to [Object.toString] will return "<proxy #id>" (where `id` is the remote object ID)
|
||||||
* instead of invoking the remote `toString()` method on the object.
|
* instead of invoking the remote `toString()` method on the object.
|
||||||
*/
|
*/
|
||||||
fun enableToString(enableDetailedToString: Boolean)
|
fun enableToString(enabled: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permits calls to [RemoteObject.waitForLastResponse] and [RemoteObject.waitForResponse] to actually wait for a response.
|
* Permits calls to [Object.hashCode] to actually return the `hashCode()` method on the object.
|
||||||
*
|
*
|
||||||
* You must be in ASYNC mode already for this to work. There will be undefined errors if you do not enable waiting
|
* @param enabled If false, calls to [Object.hashCode] will return "id" (where `id` is the remote object ID)
|
||||||
* BEFORE calling the method you want to wait for
|
* instead of invoking the remote `hashCode()` method on the object.
|
||||||
*
|
|
||||||
* @param enableWaiting if true, you want wait for the method results. If false, undefined errors can happen while waiting
|
|
||||||
*/
|
*/
|
||||||
fun enableWaitingForResponse(enableWaiting: Boolean)
|
fun enableHashCode(enabled: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for the response to the last method invocation to be received or the response timeout to be reached.
|
* Permits calls to [Object.equals] to actually return the `equals()` method on the object.
|
||||||
*
|
*
|
||||||
* You must be in ASYNC mode + enabled waiting for this to work. There will be undefined errors if you do not enable waiting BEFORE
|
* @param enabled If false, calls to [Object.equals] will compare the "id" (where `id` is the remote object ID)
|
||||||
* calling the method you want to wait for
|
* instead of invoking the remote `equals()` method on the object.
|
||||||
*
|
|
||||||
* @return the response of the last method invocation
|
|
||||||
*/
|
*/
|
||||||
suspend fun waitForLastResponse(): Any?
|
fun enableEquals(enabled: Boolean)
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for the specified method invocation response to be received or the response timeout to be reached.
|
|
||||||
*
|
|
||||||
* You must be in ASYNC mode + enabled waiting for this to work. There will be undefined errors if you do not enable waiting BEFORE
|
|
||||||
* calling the method you want to wait for
|
|
||||||
*
|
|
||||||
* @param responseId usually this is the response ID obtained via [RemoteObject.lastResponseId]
|
|
||||||
*
|
|
||||||
* @return the response of the last method invocation
|
|
||||||
*/
|
|
||||||
suspend fun waitForResponse(responseId: Int): Any?
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Causes this RemoteObject to stop listening to the connection for method invocation response messages.
|
* Causes this RemoteObject to stop listening to the connection for method invocation response messages.
|
||||||
|
@ -216,9 +216,10 @@ class RemoteObjectStorage(val logger: KLogger) {
|
|||||||
* Registers an object to allow a remote connection access to this object via the specified ID
|
* Registers an object to allow a remote connection access to this object via the specified ID
|
||||||
*
|
*
|
||||||
* @param objectId Must not be <= 0 or > 65535
|
* @param objectId Must not be <= 0 or > 65535
|
||||||
|
*
|
||||||
* @return true if successful, false if there was an error
|
* @return true if successful, false if there was an error
|
||||||
*/
|
*/
|
||||||
fun register(objectId: Int, `object`: Any): Boolean {
|
fun register(`object`: Any, objectId: Int): Boolean {
|
||||||
validate(objectId)
|
validate(objectId)
|
||||||
|
|
||||||
objectMap.put(objectId, `object`)
|
objectMap.put(objectId, `object`)
|
||||||
@ -231,12 +232,13 @@ class RemoteObjectStorage(val logger: KLogger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an object. The remote connection will no longer be able to access it.
|
* Removes an object. The remote connection will no longer be able to access it. This object may, or may not exist
|
||||||
*/
|
*/
|
||||||
fun <T> remove(objectId: Int): T {
|
fun <T> remove(objectId: Int): T? {
|
||||||
validate(objectId)
|
validate(objectId)
|
||||||
|
|
||||||
val rmiObject = objectMap.remove(objectId) as T
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val rmiObject = objectMap.remove(objectId) as T?
|
||||||
returnId(objectId)
|
returnId(objectId)
|
||||||
|
|
||||||
logger.trace {
|
logger.trace {
|
||||||
@ -264,9 +266,11 @@ class RemoteObjectStorage(val logger: KLogger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This object may, or may not exist
|
||||||
|
*
|
||||||
* @return the object registered with the specified ID.
|
* @return the object registered with the specified ID.
|
||||||
*/
|
*/
|
||||||
operator fun get(objectId: Int): Any {
|
operator fun get(objectId: Int): Any? {
|
||||||
validate(objectId)
|
validate(objectId)
|
||||||
|
|
||||||
return objectMap[objectId]
|
return objectMap[objectId]
|
||||||
|
@ -16,28 +16,30 @@
|
|||||||
package dorkbox.network.rmi
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.Connection
|
||||||
import dorkbox.network.other.SuspendWaiter
|
import dorkbox.network.other.SuspendFunctionAccess
|
||||||
import dorkbox.network.other.invokeSuspendFunction
|
|
||||||
import dorkbox.network.rmi.messages.MethodRequest
|
import dorkbox.network.rmi.messages.MethodRequest
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles network communication when methods are invoked on a proxy. For NON-BLOCKING performance, the interface
|
* Handles network communication when methods are invoked on a proxy.
|
||||||
* must have the 'suspend' keyword added. If it is not present, then all method invocation will be BLOCKING.
|
|
||||||
*
|
*
|
||||||
|
* For NON-BLOCKING performance, the RMI interface must have the 'suspend' keyword added. If this keyword is not
|
||||||
|
* present, then all method invocation will be BLOCKING.
|
||||||
*
|
*
|
||||||
*
|
* @param isGlobal true if this is a global object, or false if it is connection specific
|
||||||
* @param connection this is really the network client -- there is ONLY ever 1 connection
|
|
||||||
* @param rmiSupport is used to provide RMI support
|
|
||||||
* @param rmiObjectId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
|
* @param rmiObjectId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID
|
||||||
|
* @param connection this is really the network client -- there is ONLY ever 1 connection
|
||||||
|
* @param proxyString this is the name assigned to the proxy [toString] method
|
||||||
|
* @param rmiSupportCache is used to provide RMI support
|
||||||
* @param cachedMethods this is the methods available for the specified class
|
* @param cachedMethods this is the methods available for the specified class
|
||||||
*/
|
*/
|
||||||
class RmiClient(val isGlobal: Boolean,
|
internal class RmiClient(val isGlobal: Boolean,
|
||||||
val rmiObjectId: Int,
|
val rmiObjectId: Int,
|
||||||
private val connection: Connection,
|
private val connection: Connection,
|
||||||
private val proxyString: String,
|
private val proxyString: String,
|
||||||
@ -48,34 +50,35 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
private val methods = RmiUtils.getMethods(RemoteObject::class.java)
|
private val methods = RmiUtils.getMethods(RemoteObject::class.java)
|
||||||
|
|
||||||
private val closeMethod = methods.find { it.name == "close" }
|
private val closeMethod = methods.find { it.name == "close" }
|
||||||
|
private val toStringMethod = methods.find { it.name == "toString" }
|
||||||
|
private val hashCodeMethod = methods.find { it.name == "hashCode" }
|
||||||
|
private val equalsMethod = methods.find { it.name == "equals" }
|
||||||
|
|
||||||
|
private val enableToStringMethod = methods.find { it.name == "enableToString" }
|
||||||
|
private val enableHashCodeMethod = methods.find { it.name == "enableHashCode" }
|
||||||
|
private val enableEqualsMethod = methods.find { it.name == "enableEquals" }
|
||||||
|
|
||||||
|
|
||||||
private val setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" }
|
private val setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" }
|
||||||
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
|
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
|
||||||
private val setAsyncMethod = methods.find { it.name == "setAsync" }
|
private val setAsyncMethod = methods.find { it.name == "setAsync" }
|
||||||
private val getAsyncMethod = methods.find { it.name == "getAsync" }
|
private val getAsyncMethod = methods.find { it.name == "getAsync" }
|
||||||
private val enableToStringMethod = methods.find { it.name == "enableToString" }
|
|
||||||
private val enableWaitingForResponseMethod = methods.find { it.name == "enableWaitingForResponse" }
|
|
||||||
private val waitForLastResponseMethod = methods.find { it.name == "waitForLastResponse" }
|
|
||||||
private val getLastResponseIdMethod = methods.find { it.name == "getLastResponseId" }
|
|
||||||
private val waitForResponseMethod = methods.find { it.name == "waitForResponse" }
|
|
||||||
private val toStringMethod = methods.find { it.name == "toString" }
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
private val EMPTY_ARRAY: Array<Any> = Collections.EMPTY_LIST.toTypedArray() as Array<Any>
|
private val EMPTY_ARRAY: Array<Any> = Collections.EMPTY_LIST.toTypedArray() as Array<Any>
|
||||||
}
|
}
|
||||||
|
|
||||||
private val responseWaiter = SuspendWaiter()
|
|
||||||
|
|
||||||
private var timeoutMillis: Long = 3000
|
private var timeoutMillis: Long = 3000
|
||||||
private var isAsync = false
|
private var isAsync = false
|
||||||
private var allowWaiting = false
|
|
||||||
private var enableToString = false
|
private var enableToString = false
|
||||||
|
private var enableHashCode = false
|
||||||
|
private var enableEquals = false
|
||||||
|
|
||||||
// this is really a a short!
|
// if we are ASYNC, then this method immediately returns
|
||||||
@Volatile
|
private suspend fun sendRequest(method: Method, args: Array<Any>): Any? {
|
||||||
private var previousResponseId: Int = 0
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun invokeSuspend(method: Method, args: Array<Any>): Any? {
|
|
||||||
// there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods --
|
// there is a STRANGE problem, where if we DO NOT respond/reply to method invocation, and immediate invoke multiple methods --
|
||||||
// the "server" side can have out-of-order method invocation. There are 2 ways to solve this
|
// the "server" side can have out-of-order method invocation. There are 2 ways to solve this
|
||||||
// 1) make the "server" side single threaded
|
// 1) make the "server" side single threaded
|
||||||
@ -87,18 +90,14 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
|
|
||||||
val responseStorage = rmiSupportCache.getResponseStorage()
|
val responseStorage = rmiSupportCache.getResponseStorage()
|
||||||
|
|
||||||
// If we are async, we ignore the response.... FOR NOW. The response, even if there is NOT one (ie: not void) will always return
|
// If we are async, we ignore the response....
|
||||||
// a thing (so we will know when to stop blocking).
|
// The response, even if there is NOT one (ie: not void) will always return a thing (so we will know when to stop blocking).
|
||||||
val responseId = responseStorage.prep(rmiObjectId, responseWaiter)
|
val rmiWaiter = responseStorage.prep(rmiObjectId)
|
||||||
|
|
||||||
// so we can query for async, if we want to necessary
|
|
||||||
previousResponseId = responseId
|
|
||||||
|
|
||||||
|
|
||||||
val invokeMethod = MethodRequest()
|
val invokeMethod = MethodRequest()
|
||||||
invokeMethod.isGlobal = isGlobal
|
invokeMethod.isGlobal = isGlobal
|
||||||
invokeMethod.objectId = rmiObjectId
|
invokeMethod.objectId = rmiObjectId
|
||||||
invokeMethod.responseId = responseId
|
invokeMethod.responseId = rmiWaiter.id
|
||||||
invokeMethod.args = args
|
invokeMethod.args = args
|
||||||
|
|
||||||
// which method do we access? We always want to access the IMPLEMENTATION (if available!). we know that this will always succeed
|
// which method do we access? We always want to access the IMPLEMENTATION (if available!). we know that this will always succeed
|
||||||
@ -107,21 +106,26 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
|
|
||||||
connection.send(invokeMethod)
|
connection.send(invokeMethod)
|
||||||
|
|
||||||
// if we are async, then this will immediately return!
|
|
||||||
return try {
|
// if we are async, then this will immediately return
|
||||||
val result = responseStorage.waitForReply(allowWaiting, isAsync, rmiObjectId, responseId, responseWaiter, timeoutMillis)
|
val result = responseStorage.waitForReply(isAsync, rmiObjectId, rmiWaiter, timeoutMillis)
|
||||||
if (result is Exception) {
|
when (result) {
|
||||||
throw result
|
RmiResponseStorage.TIMEOUT_EXCEPTION -> {
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
} catch (ex: TimeoutException) {
|
|
||||||
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
|
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
|
||||||
}
|
}
|
||||||
|
is Exception -> {
|
||||||
|
throw result
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DuplicatedCode")
|
@Suppress("DuplicatedCode")
|
||||||
@Throws(Exception::class)
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
|
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
|
||||||
if (method.declaringClass == RemoteObject::class.java) {
|
if (method.declaringClass == RemoteObject::class.java) {
|
||||||
// manage all of the RemoteObject proxy methods
|
// manage all of the RemoteObject proxy methods
|
||||||
@ -130,6 +134,7 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
rmiSupportCache.removeProxyObject(rmiObjectId)
|
rmiSupportCache.removeProxyObject(rmiObjectId)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
setResponseTimeoutMethod -> {
|
setResponseTimeoutMethod -> {
|
||||||
timeoutMillis = (args!![0] as Int).toLong()
|
timeoutMillis = (args!![0] as Int).toLong()
|
||||||
return null
|
return null
|
||||||
@ -137,6 +142,7 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
getResponseTimeoutMethod -> {
|
getResponseTimeoutMethod -> {
|
||||||
return timeoutMillis.toInt()
|
return timeoutMillis.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
getAsyncMethod -> {
|
getAsyncMethod -> {
|
||||||
return isAsync
|
return isAsync
|
||||||
}
|
}
|
||||||
@ -144,52 +150,39 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
isAsync = args!![0] as Boolean
|
isAsync = args!![0] as Boolean
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
enableToStringMethod -> {
|
enableToStringMethod -> {
|
||||||
enableToString = args!![0] as Boolean
|
enableToString = args!![0] as Boolean
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
getLastResponseIdMethod -> {
|
enableHashCodeMethod -> {
|
||||||
// only ASYNC can wait for responses
|
enableHashCode = args!![0] as Boolean
|
||||||
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually get the response ID." }
|
|
||||||
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
|
|
||||||
"calling the method that you want to wait for the respose to" }
|
|
||||||
|
|
||||||
return previousResponseId
|
|
||||||
}
|
|
||||||
enableWaitingForResponseMethod -> {
|
|
||||||
allowWaiting = args!![0] as Boolean
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
waitForLastResponseMethod -> {
|
enableEqualsMethod -> {
|
||||||
// only ASYNC can wait for responses
|
enableEquals = args!![0] as Boolean
|
||||||
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually wait for a response." }
|
return null
|
||||||
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
|
|
||||||
"calling the method that you want to wait for the respose to" }
|
|
||||||
|
|
||||||
val maybeContinuation = args?.lastOrNull() as Continuation<*>
|
|
||||||
|
|
||||||
// this is a suspend method, so we don't need extra checks
|
|
||||||
return invokeSuspendFunction(maybeContinuation) {
|
|
||||||
rmiSupportCache.getResponseStorage().waitForReplyManually(rmiObjectId, previousResponseId, responseWaiter)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
waitForResponseMethod -> {
|
|
||||||
// only ASYNC can wait for responses
|
|
||||||
check(isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to manually wait for a response." }
|
|
||||||
check(allowWaiting) { "This RemoteObject does not allow waiting for responses. You must enable this BEFORE " +
|
|
||||||
"calling the method that you want to wait for the respose to" }
|
|
||||||
|
|
||||||
val maybeContinuation = args?.lastOrNull() as Continuation<*>
|
|
||||||
|
|
||||||
// this is a suspend method, so we don't need extra checks
|
|
||||||
return invokeSuspendFunction(maybeContinuation) {
|
|
||||||
rmiSupportCache.getResponseStorage().waitForReplyManually(rmiObjectId, args[0] as Int, responseWaiter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}")
|
else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}")
|
||||||
}
|
}
|
||||||
} else if (!enableToString && method == toStringMethod) {
|
} else {
|
||||||
return proxyString
|
when (method) {
|
||||||
|
toStringMethod -> if (!enableToString) return proxyString // otherwise, the RMI round trip logic is done for toString()
|
||||||
|
hashCodeMethod -> if (!enableHashCode) return rmiObjectId // otherwise, the RMI round trip logic is done for hashCode()
|
||||||
|
equalsMethod -> {
|
||||||
|
val other = args!![0]
|
||||||
|
if (other !is RmiClient) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableEquals) {
|
||||||
|
return rmiObjectId == other.rmiObjectId
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, the RMI round trip logic is done for equals()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
|
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
|
||||||
@ -201,11 +194,11 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
if (maybeContinuation is Continuation<*>) {
|
if (maybeContinuation is Continuation<*>) {
|
||||||
val argsWithoutContinuation = args.take(args.size - 1)
|
val argsWithoutContinuation = args.take(args.size - 1)
|
||||||
invokeSuspendFunction(maybeContinuation) {
|
invokeSuspendFunction(maybeContinuation) {
|
||||||
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
|
sendRequest(method, argsWithoutContinuation.toTypedArray())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
invokeSuspend(method, args ?: EMPTY_ARRAY)
|
sendRequest(method, args ?: EMPTY_ARRAY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,11 +242,11 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
return if (maybeContinuation is Continuation<*>) {
|
return if (maybeContinuation is Continuation<*>) {
|
||||||
val argsWithoutContinuation = args.take(args.size - 1)
|
val argsWithoutContinuation = args.take(args.size - 1)
|
||||||
invokeSuspendFunction(maybeContinuation) {
|
invokeSuspendFunction(maybeContinuation) {
|
||||||
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
|
sendRequest(method, argsWithoutContinuation.toTypedArray())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
invokeSuspend(method, args ?: EMPTY_ARRAY)
|
sendRequest(method, args ?: EMPTY_ARRAY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -283,4 +276,12 @@ class RmiClient(val isGlobal: Boolean,
|
|||||||
|
|
||||||
return rmiObjectId == other.rmiObjectId
|
return rmiObjectId == other.rmiObjectId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun invokeSuspendFunction(continuation: Continuation<*>, suspendFunction: suspend () -> Any?): Any? {
|
||||||
|
return try {
|
||||||
|
SuspendFunctionAccess.invokeSuspendFunction(suspendFunction, continuation)
|
||||||
|
} catch (e: InvocationTargetException) {
|
||||||
|
throw e.cause!!
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package dorkbox.network.rmi
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
|
import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue
|
||||||
import dorkbox.network.other.SuspendWaiter
|
|
||||||
import dorkbox.network.rmi.messages.MethodResponse
|
import dorkbox.network.rmi.messages.MethodResponse
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.agrona.collections.Hashing
|
import org.agrona.collections.Hashing
|
||||||
@ -14,31 +13,90 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
|
|||||||
import kotlin.concurrent.read
|
import kotlin.concurrent.read
|
||||||
import kotlin.concurrent.write
|
import kotlin.concurrent.write
|
||||||
|
|
||||||
|
internal data class RmiWaiter(val id: Int) {
|
||||||
|
// this is bi-directional waiting. The method names to not reflect this, however there is no possibility of race conditions w.r.t. waiting
|
||||||
|
// https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified
|
||||||
|
// https://kotlinlang.org/docs/reference/coroutines/channels.html
|
||||||
|
|
||||||
|
// "receive' suspends until another coroutine invokes "send"
|
||||||
|
// and
|
||||||
|
// "send" suspends until another coroutine invokes "receive".
|
||||||
|
//
|
||||||
|
// these are wrapped in a try/catch, because cancel will cause exceptions to be thrown (which we DO NOT want)
|
||||||
|
@Volatile
|
||||||
|
var channel: Channel<Unit> = Channel(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this will replace the waiter if it was cancelled (waiters are not valid if cancelled)
|
||||||
|
*/
|
||||||
|
fun prep() {
|
||||||
|
if (channel.isClosedForReceive && channel.isClosedForSend) {
|
||||||
|
println("renew waiter")
|
||||||
|
channel = Channel(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun doNotify() {
|
||||||
|
println("notified waiter")
|
||||||
|
try {
|
||||||
|
channel.send(Unit)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun doWait() {
|
||||||
|
println("waiting waiter")
|
||||||
|
try {
|
||||||
|
channel.receive()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
println("delay is cancelling suspending coroutine")
|
||||||
|
try {
|
||||||
|
channel.cancel()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the "pending response" from method invocation.
|
* Manages the "pending response" from method invocation.
|
||||||
*
|
*
|
||||||
* response ID's and the memory they hold will leak if the response never arrives!
|
* response ID's and the memory they hold will leak if the response never arrives!
|
||||||
*/
|
*/
|
||||||
class RmiResponseStorage(private val actionDispatch: CoroutineScope) {
|
internal class RmiResponseStorage(private val actionDispatch: CoroutineScope) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TIMEOUT_EXCEPTION = Exception()
|
||||||
|
}
|
||||||
|
|
||||||
// Response ID's are for ALL in-flight RMI on the network stack. instead of limited to (originally) 64, we are now limited to 65,535
|
// Response ID's are for ALL in-flight RMI on the network stack. instead of limited to (originally) 64, we are now limited to 65,535
|
||||||
// these are just looped around in a ring buffer.
|
// these are just looped around in a ring buffer.
|
||||||
// These are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire
|
// These are stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire
|
||||||
private val rmiResponseIds = MultithreadConcurrentQueue<Int>(65535)
|
// 32,000 IN FLIGHT RMI method invocations is PLENTY
|
||||||
|
private val maxValuesInCache = Short.MAX_VALUE.toInt()
|
||||||
|
|
||||||
|
private val rmiWaiterCache = MultithreadConcurrentQueue<RmiWaiter>(maxValuesInCache)
|
||||||
|
|
||||||
private val pendingLock = ReentrantReadWriteLock()
|
private val pendingLock = ReentrantReadWriteLock()
|
||||||
private val pending = Int2NullableObjectHashMap<Any>(32, Hashing.DEFAULT_LOAD_FACTOR, true)
|
private val pending = Int2NullableObjectHashMap<Any>(32, Hashing.DEFAULT_LOAD_FACTOR, true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint!
|
// create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint!
|
||||||
val ids = IntArrayList()
|
val ids = IntArrayList(maxValuesInCache, Integer.MIN_VALUE)
|
||||||
for (id in Short.MIN_VALUE..Short.MAX_VALUE) {
|
// ZERO is special, and is never added!
|
||||||
|
for (id in 1..Short.MAX_VALUE) {
|
||||||
ids.addInt(id)
|
ids.addInt(id)
|
||||||
}
|
}
|
||||||
ids.shuffle()
|
ids.shuffle()
|
||||||
|
|
||||||
// populate the array of randomly assigned ID's.
|
// populate the array of randomly assigned ID's + waiters.
|
||||||
ids.forEach {
|
ids.forEach {
|
||||||
rmiResponseIds.offer(it)
|
rmiWaiterCache.offer(RmiWaiter(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,95 +109,87 @@ class RmiResponseStorage(private val actionDispatch: CoroutineScope) {
|
|||||||
|
|
||||||
val pendingId = RmiUtils.packShorts(objectId, responseId)
|
val pendingId = RmiUtils.packShorts(objectId, responseId)
|
||||||
|
|
||||||
|
println("pending result received")
|
||||||
|
|
||||||
val previous = pendingLock.write { pending.put(pendingId, result) }
|
val previous = pendingLock.write { pending.put(pendingId, result) }
|
||||||
|
|
||||||
// if NULL, since either we don't exist, or it was cancelled
|
// if NULL, since either we don't exist, or it was cancelled
|
||||||
if (previous is SuspendWaiter) {
|
if (previous is RmiWaiter) {
|
||||||
// this means we were NOT timed out!
|
// this means we were NOT timed out! If we were cancelled, then this does nothing.
|
||||||
previous.doNotify()
|
previous.doNotify()
|
||||||
|
|
||||||
|
// since this was the FIRST one to trigger, return it to the cache.
|
||||||
|
rmiWaiterCache.offer(previous)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// always return the responseId! It will (hopefully) be a while before this ID is used again
|
/**
|
||||||
rmiResponseIds.offer(responseId)
|
* gets the RmiWaiter (id + waiter)
|
||||||
}
|
*/
|
||||||
|
internal fun prep(rmiObjectId: Int): RmiWaiter {
|
||||||
|
val responseRmi = rmiWaiterCache.poll()
|
||||||
|
|
||||||
fun prep(rmiObjectId: Int, responseWaiter: SuspendWaiter): Int {
|
// this will replace the waiter if it was cancelled (waiters are not valid if cancelled)
|
||||||
val responseId = rmiResponseIds.poll()
|
responseRmi.prep()
|
||||||
|
|
||||||
// we pack them together so we can fully use the range of ints, so we can service ALL rmi requests in a single spot
|
// we pack them together so we can fully use the range of ints, so we can service ALL rmi requests in a single spot
|
||||||
pendingLock.write { pending[RmiUtils.packShorts(rmiObjectId, responseId)] = responseWaiter }
|
pendingLock.write { pending[RmiUtils.packShorts(rmiObjectId, responseRmi.id)] = responseRmi }
|
||||||
|
|
||||||
return responseId
|
return responseRmi
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitForReply(allowWaiting: Boolean, isAsync: Boolean, rmiObjectId: Int, responseId: Int,
|
/**
|
||||||
responseWaiter: SuspendWaiter, timeoutMillis: Long): Any? {
|
* @return the result (can be null) or timeout exception
|
||||||
|
*/
|
||||||
|
suspend fun waitForReply(isAsync: Boolean, rmiObjectId: Int, rmiWaiter: RmiWaiter, timeoutMillis: Long): Any? {
|
||||||
|
|
||||||
val pendingId = RmiUtils.packShorts(rmiObjectId, responseId)
|
val pendingId = RmiUtils.packShorts(rmiObjectId, rmiWaiter.id)
|
||||||
|
|
||||||
var delayJobForTimeout: Job? = null
|
// NOTE: we ALWAYS send a response from the remote end.
|
||||||
|
|
||||||
if (!(isAsync && allowWaiting) && timeoutMillis > 0L) {
|
|
||||||
// always launch a "cancel" coroutine, unless we want to wait forever
|
|
||||||
delayJobForTimeout = actionDispatch.launch {
|
|
||||||
delay(timeoutMillis)
|
|
||||||
|
|
||||||
val previous = pendingLock.write {
|
|
||||||
val prev = pending.remove(pendingId)
|
|
||||||
if (prev is SuspendWaiter) {
|
|
||||||
pending[pendingId] = TimeoutException("Response timed out.")
|
|
||||||
}
|
|
||||||
|
|
||||||
prev
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we are NOT SuspendWaiter, then it means we had a result!
|
|
||||||
//
|
//
|
||||||
// If there are tight timing issues, then we err on the side of "you timed out"
|
// 'async' -> DO NOT WAIT
|
||||||
|
// 'timeout > 0' -> WAIT
|
||||||
|
// 'timeout == 0' -> same as async (DO NOT WAIT)
|
||||||
|
val returnImmediately = isAsync || timeoutMillis <= 0L
|
||||||
|
|
||||||
if (!isAsync) {
|
if (returnImmediately) {
|
||||||
// we only cancel waiting because when NON-ASYNC
|
return null
|
||||||
if (previous is SuspendWaiter) {
|
|
||||||
previous.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val responseTimeoutJob = actionDispatch.launch {
|
||||||
|
delay(timeoutMillis) // this will always wait
|
||||||
|
|
||||||
|
// check if we have a result or not
|
||||||
|
val maybeResult = pendingLock.read { pending[pendingId] }
|
||||||
|
if (maybeResult is RmiWaiter) {
|
||||||
|
System.err.println("TIMEOUT $pendingId")
|
||||||
|
// maybeResult.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (isAsync) {
|
// wait for the response.
|
||||||
null
|
//
|
||||||
} else {
|
// If the response is ALREADY here, the doWait() returns instantly (with result)
|
||||||
waitForReplyManually(pendingId, responseWaiter, delayJobForTimeout)
|
// if no response yet, it will suspend and either
|
||||||
}
|
// A) get response
|
||||||
|
// B) timeout
|
||||||
|
rmiWaiter.doWait()
|
||||||
|
|
||||||
|
// always cancel the timeout
|
||||||
|
responseTimeoutJob.cancel()
|
||||||
|
|
||||||
|
val resultOrWaiter = pendingLock.write { pending.remove(pendingId) }
|
||||||
|
if (resultOrWaiter is RmiWaiter) {
|
||||||
|
// since this was the FIRST one to trigger, return it to the cache.
|
||||||
|
rmiWaiterCache.offer(resultOrWaiter)
|
||||||
|
return TIMEOUT_EXCEPTION
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is called when we MANUALLY want to wait for a reply as part of async!
|
return resultOrWaiter
|
||||||
// A timeout of 0 means we wait forever
|
|
||||||
suspend fun waitForReplyManually(rmiObjectId: Int, responseId: Int, responseWaiter: SuspendWaiter): Any? {
|
|
||||||
val pendingId = RmiUtils.packShorts(rmiObjectId, responseId)
|
|
||||||
return waitForReplyManually(pendingId, responseWaiter, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// we have to be careful when we resume, because SOMEONE ELSE'S METHOD RESPONSE can resume us (but only from the same object)!
|
|
||||||
private suspend fun waitForReplyManually(pendingId: Int,responseWaiter: SuspendWaiter, delayJobForTimeout: Job?): Any? {
|
|
||||||
while(true) {
|
|
||||||
val checkResult = pendingLock.read { pending[pendingId] }
|
|
||||||
if (checkResult !is SuspendWaiter) {
|
|
||||||
// this means we have correct data! (or it was an exception) we can safely remove the data from the map
|
|
||||||
pendingLock.write { pending.remove(pendingId) }
|
|
||||||
|
|
||||||
delayJobForTimeout?.cancel()
|
|
||||||
return checkResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep waiting, since we don't have a response yet
|
|
||||||
responseWaiter.doWait()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
rmiResponseIds.clear()
|
rmiWaiterCache.clear()
|
||||||
pendingLock.write { pending.clear() }
|
pendingLock.write { pending.clear() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
package dorkbox.network.rmi
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.Connection
|
||||||
import dorkbox.network.connection.Connection_
|
|
||||||
import dorkbox.network.connection.EndPoint
|
import dorkbox.network.connection.EndPoint
|
||||||
|
import dorkbox.network.connection.ListenerManager
|
||||||
import dorkbox.network.rmi.messages.*
|
import dorkbox.network.rmi.messages.*
|
||||||
import dorkbox.network.serialization.NetworkSerializationManager
|
import dorkbox.network.serialization.NetworkSerializationManager
|
||||||
import dorkbox.util.classes.ClassHelper
|
import dorkbox.util.classes.ClassHelper
|
||||||
@ -26,10 +26,9 @@ import mu.KLogger
|
|||||||
import java.lang.reflect.Proxy
|
import java.lang.reflect.Proxy
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class RmiSupport<C : Connection>(logger: KLogger,
|
internal class RmiSupport(logger: KLogger,
|
||||||
actionDispatch: CoroutineScope,
|
actionDispatch: CoroutineScope,
|
||||||
internal val serialization: NetworkSerializationManager) : RmiSupportCache(logger, actionDispatch)
|
internal val serialization: NetworkSerializationManager) : RmiSupportCache(logger, actionDispatch) {
|
||||||
{
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked
|
* Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked
|
||||||
@ -127,7 +126,7 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
/**
|
/**
|
||||||
* called on "client"
|
* called on "client"
|
||||||
*/
|
*/
|
||||||
private fun onGenericObjectResponse(endPoint: EndPoint<Connection_>, connection: Connection_, logger: KLogger,
|
private fun onGenericObjectResponse(endPoint: EndPoint<*>, connection: Connection, logger: KLogger,
|
||||||
isGlobal: Boolean, rmiId: Int, callback: suspend (Any) -> Unit,
|
isGlobal: Boolean, rmiId: Int, callback: suspend (Any) -> Unit,
|
||||||
rmiSupportCache: RmiSupportCache, serialization: NetworkSerializationManager) {
|
rmiSupportCache: RmiSupportCache, serialization: NetworkSerializationManager) {
|
||||||
|
|
||||||
@ -162,30 +161,90 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
// this is used for all connection specific ones as well.
|
// this is used for all connection specific ones as well.
|
||||||
private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger)
|
private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
internal fun <Iface> registerCallback(callback: suspend (Iface) -> Unit): Int {
|
internal fun <Iface> registerCallback(callback: suspend (Iface) -> Unit): Int {
|
||||||
return remoteObjectCreationCallbacks.register(callback)
|
return remoteObjectCreationCallbacks.register(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeCallback(callbackId: Int): suspend (Any) -> Unit {
|
private fun removeCallback(callbackId: Int): suspend (Any) -> Unit {
|
||||||
return remoteObjectCreationCallbacks.remove(callbackId)
|
// callback's area always correct, because we track them ourselves.
|
||||||
|
return remoteObjectCreationCallbacks.remove(callbackId)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get's the implementation object based on if it is global, or not global
|
* @return the implementation object based on if it is global, or not global
|
||||||
*/
|
*/
|
||||||
fun getImplObject(isGlobal: Boolean, rmiObjectId: Int, connection: Connection_): Any {
|
fun <T> getImplObject(isGlobal: Boolean, rmiObjectId: Int, connection: Connection): T? {
|
||||||
return if (isGlobal) getImplObject(rmiObjectId) else connection.rmiSupport().getImplObject(rmiObjectId)
|
return if (isGlobal) getImplObject(rmiObjectId) else connection.rmiConnectionSupport.getImplObject(rmiObjectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the newly registered RMI ID for this object. [RemoteObjectStorage.INVALID_RMI] means it was invalid (an error log will be emitted)
|
||||||
|
*/
|
||||||
|
fun saveImplObject(logger: KLogger, `object`: Any): Int {
|
||||||
|
val rmiId = saveImplObject(`object`)
|
||||||
|
|
||||||
|
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
|
||||||
|
// this means we could register this object.
|
||||||
|
|
||||||
|
// next, scan this object to see if there are any RMI fields
|
||||||
|
scanImplForRmiFields(logger, `object`) {
|
||||||
|
saveImplObject(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error("Trying to create an RMI object with the INVALID_RMI id!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rmiId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the true if it was a success saving this object. False means it was invalid (an error log will be emitted)
|
||||||
|
*/
|
||||||
|
fun saveImplObject(logger: KLogger, `object`: Any, objectId: Int): Boolean {
|
||||||
|
val rmiSuccess = saveImplObject(`object`, objectId)
|
||||||
|
|
||||||
|
if (rmiSuccess) {
|
||||||
|
// this means we could register this object.
|
||||||
|
|
||||||
|
// next, scan this object to see if there are any RMI fields
|
||||||
|
scanImplForRmiFields(logger, `object`) {
|
||||||
|
saveImplObject(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error("Trying to save an RMI object ${`object`.javaClass} with invalid id $objectId")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rmiSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the removed object. If null, an error log will be emitted
|
||||||
|
*/
|
||||||
|
fun <T> removeImplObject(logger: KLogger, objectId: Int): T? {
|
||||||
|
val success = removeImplObject<Any>(objectId)
|
||||||
|
if (success == null) {
|
||||||
|
logger.error("Error trying to remove an RMI impl object id $objectId.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return success as T?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
super.close()
|
super.close()
|
||||||
remoteObjectCreationCallbacks.close()
|
remoteObjectCreationCallbacks.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* on the "client" to get a global remote object (that exists on the server)
|
* on the connection+client to get a global remote object (that exists on the server)
|
||||||
*/
|
*/
|
||||||
fun <Iface> getGlobalRemoteObject(connection: C, endPoint: EndPoint<C>, objectId: Int, interfaceClass: Class<Iface>): Iface {
|
fun <Iface> getGlobalRemoteObject(connection: Connection, endPoint: EndPoint<*>, objectId: Int, interfaceClass: Class<Iface>): Iface {
|
||||||
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
|
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
|
||||||
|
|
||||||
// so we can just instantly create the proxy object (or get the cached one)
|
// so we can just instantly create the proxy object (or get the cached one)
|
||||||
@ -199,35 +258,17 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
return proxyObject as Iface
|
return proxyObject as Iface
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* on the "client" to create a global remote object (that exists on the server)
|
|
||||||
*/
|
|
||||||
suspend fun <Iface> createGlobalRemoteObject(connection: Connection, interfaceClassId: Int, callback: suspend (Iface) -> Unit) {
|
|
||||||
val callbackId = registerCallback(callback)
|
|
||||||
|
|
||||||
// There is no rmiID yet, because we haven't created it!
|
|
||||||
val message = GlobalObjectCreateRequest(RmiUtils.packShorts(interfaceClassId, callbackId))
|
|
||||||
|
|
||||||
// We use a callback to notify us when the object is ready. We can't "create this on the fly" because we
|
|
||||||
// have to wait for the object to be created + ID to be assigned on the remote system BEFORE we can create the proxy instance here.
|
|
||||||
|
|
||||||
// this means we are creating a NEW object on the server
|
|
||||||
connection.send(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages ALL OF THE RMI stuff!
|
* Manages ALL OF THE RMI stuff!
|
||||||
*/
|
*/
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
suspend fun manage(endPoint: EndPoint<Connection_>, connection: Connection_, message: Any, logger: KLogger) {
|
suspend fun manage(endPoint: EndPoint<*>, connection: Connection, message: Any, logger: KLogger) {
|
||||||
when (message) {
|
when (message) {
|
||||||
is ConnectionObjectCreateRequest -> {
|
is ConnectionObjectCreateRequest -> {
|
||||||
/**
|
/**
|
||||||
* called on "server"
|
* called on "server"
|
||||||
*/
|
*/
|
||||||
connection.rmiSupport().onConnectionObjectCreateRequest(endPoint, connection, message, logger)
|
connection.rmiConnectionSupport.onConnectionObjectCreateRequest(endPoint, connection, message, logger)
|
||||||
}
|
}
|
||||||
is ConnectionObjectCreateResponse -> {
|
is ConnectionObjectCreateResponse -> {
|
||||||
/**
|
/**
|
||||||
@ -265,7 +306,19 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
val isGlobal: Boolean = message.isGlobal
|
val isGlobal: Boolean = message.isGlobal
|
||||||
val cachedMethod = message.cachedMethod
|
val cachedMethod = message.cachedMethod
|
||||||
|
|
||||||
val implObject = getImplObject(isGlobal, objectId, connection)
|
val implObject = getImplObject<Any>(isGlobal, objectId, connection)
|
||||||
|
|
||||||
|
if (implObject == null) {
|
||||||
|
logger.error("Unable to resolve implementation object for [global=$isGlobal, objectID=$objectId, connection=$connection")
|
||||||
|
|
||||||
|
val invokeMethodResult = MethodResponse()
|
||||||
|
invokeMethodResult.objectId = objectId
|
||||||
|
invokeMethodResult.responseId = message.responseId
|
||||||
|
invokeMethodResult.result = NullPointerException("Remote object for proxy [global=$isGlobal, objectID=$objectId] does not exist.")
|
||||||
|
|
||||||
|
connection.send(invokeMethodResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace {
|
logger.trace {
|
||||||
var argString = ""
|
var argString = ""
|
||||||
@ -295,8 +348,6 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
// args!! is safe to do here (even though it doesn't make sense)
|
// args!! is safe to do here (even though it doesn't make sense)
|
||||||
result = cachedMethod.invoke(connection, implObject, message.args!!)
|
result = cachedMethod.invoke(connection, implObject, message.args!!)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", ex)
|
|
||||||
|
|
||||||
result = ex.cause
|
result = ex.cause
|
||||||
// added to prevent a stack overflow when references is false, (because 'cause' == "this").
|
// added to prevent a stack overflow when references is false, (because 'cause' == "this").
|
||||||
// See:
|
// See:
|
||||||
@ -306,6 +357,13 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
} else {
|
} else {
|
||||||
result.initCause(null)
|
result.initCause(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only remove stuff if our logger is NOT on trace (so normal logs will not show extra info, trace will show extra info)
|
||||||
|
if (!logger.isTraceEnabled) {
|
||||||
|
ListenerManager.cleanStackTrace(result as Throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
val invokeMethodResult = MethodResponse()
|
val invokeMethodResult = MethodResponse()
|
||||||
@ -325,28 +383,13 @@ class RmiSupport<C : Connection>(logger: KLogger,
|
|||||||
/**
|
/**
|
||||||
* called on "server"
|
* called on "server"
|
||||||
*/
|
*/
|
||||||
private suspend fun onGlobalObjectCreateRequest(
|
private suspend fun onGlobalObjectCreateRequest(endPoint: EndPoint<*>, connection: Connection, message: GlobalObjectCreateRequest, logger: KLogger) {
|
||||||
endPoint: EndPoint<Connection_>, connection: Connection_, message: GlobalObjectCreateRequest, logger: KLogger) {
|
|
||||||
|
|
||||||
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
|
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
|
||||||
val callbackId = RmiUtils.unpackRight(message.packedIds)
|
val callbackId = RmiUtils.unpackRight(message.packedIds)
|
||||||
|
|
||||||
// We have to lookup the iface, since the proxy object requires it
|
// We have to lookup the iface, since the proxy object requires it
|
||||||
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
|
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
|
||||||
val rmiId = registerImplObject(implObject)
|
val rmiId = saveImplObject(logger, implObject)
|
||||||
|
|
||||||
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
|
|
||||||
// this means we could register this object.
|
|
||||||
|
|
||||||
// next, scan this object to see if there are any RMI fields
|
|
||||||
scanImplForRmiFields(logger, implObject) {
|
|
||||||
registerImplObject(implObject)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error {
|
|
||||||
"Trying to create an RMI object with the INVALID_RMI id!!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
|
// we send the message ANYWAYS, because the client needs to know it did NOT succeed!
|
||||||
connection.send(GlobalObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId)))
|
connection.send(GlobalObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId)))
|
||||||
|
@ -10,22 +10,27 @@ import mu.KLogger
|
|||||||
* The impl/proxy objects CANNOT be stored in the same data structure, because their IDs are not tied to the same ID source (and there
|
* The impl/proxy objects CANNOT be stored in the same data structure, because their IDs are not tied to the same ID source (and there
|
||||||
* would be conflicts in the data structure)
|
* would be conflicts in the data structure)
|
||||||
*/
|
*/
|
||||||
open class RmiSupportCache(logger: KLogger, actionDispatch: CoroutineScope) {
|
internal open class RmiSupportCache(logger: KLogger, actionDispatch: CoroutineScope) {
|
||||||
|
|
||||||
private val responseStorage = RmiResponseStorage(actionDispatch)
|
private val responseStorage = RmiResponseStorage(actionDispatch)
|
||||||
private val implObjects = RemoteObjectStorage(logger)
|
private val implObjects = RemoteObjectStorage(logger)
|
||||||
private val proxyObjects = LockFreeIntMap<RemoteObject>()
|
private val proxyObjects = LockFreeIntMap<RemoteObject>()
|
||||||
|
|
||||||
fun registerImplObject(rmiObject: Any): Int {
|
fun saveImplObject(rmiObject: Any): Int {
|
||||||
return implObjects.register(rmiObject)
|
return implObjects.register(rmiObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImplObject(rmiId: Int): Any {
|
fun saveImplObject(rmiObject: Any, objectId: Int): Boolean {
|
||||||
return implObjects[rmiId]
|
return implObjects.register(rmiObject, objectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeImplObject(rmiId: Int) {
|
fun <T> getImplObject(rmiId: Int): T? {
|
||||||
implObjects.remove(rmiId) as Any
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return implObjects[rmiId] as T?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> removeImplObject(rmiId: Int): T? {
|
||||||
|
return implObjects.remove(rmiId) as T?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package dorkbox.network.rmi
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import dorkbox.network.connection.Connection
|
import dorkbox.network.connection.Connection
|
||||||
import dorkbox.network.connection.Connection_
|
|
||||||
import dorkbox.network.connection.EndPoint
|
import dorkbox.network.connection.EndPoint
|
||||||
import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest
|
import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest
|
||||||
import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse
|
import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse
|
||||||
@ -9,21 +8,21 @@ import dorkbox.network.serialization.NetworkSerializationManager
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import mu.KLogger
|
import mu.KLogger
|
||||||
|
|
||||||
class RmiSupportConnection<C: Connection>(logger: KLogger,
|
internal class RmiSupportConnection(logger: KLogger,
|
||||||
private val rmiGlobalSupport: RmiSupport<out Connection>,
|
val rmiGlobalSupport: RmiSupport,
|
||||||
private val serialization: NetworkSerializationManager,
|
private val serialization: NetworkSerializationManager,
|
||||||
actionDispatch: CoroutineScope) : RmiSupportCache(logger, actionDispatch) {
|
actionDispatch: CoroutineScope) : RmiSupportCache(logger, actionDispatch) {
|
||||||
|
|
||||||
/**
|
private fun <Iface> createProxyObject(isGlobalObject: Boolean,
|
||||||
* on the "client" to get a connection-specific remote object (that exists on the server)
|
connection: Connection,
|
||||||
*/
|
endPoint: EndPoint<*>,
|
||||||
fun <Iface> getRemoteObject(connection: Connection, endPoint: EndPoint<Connection_>, objectId: Int, interfaceClass: Class<Iface>): Iface {
|
objectId: Int,
|
||||||
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
|
interfaceClass: Class<Iface>) : Iface {
|
||||||
|
|
||||||
// so we can just instantly create the proxy object (or get the cached one)
|
// so we can just instantly create the proxy object (or get the cached one)
|
||||||
var proxyObject = getProxyObject(objectId)
|
var proxyObject = getProxyObject(objectId)
|
||||||
if (proxyObject == null) {
|
if (proxyObject == null) {
|
||||||
proxyObject = RmiSupport.createProxyObject(false, connection, serialization, rmiGlobalSupport, endPoint.type.simpleName, objectId, interfaceClass)
|
proxyObject = RmiSupport.createProxyObject(isGlobalObject, connection, serialization, rmiGlobalSupport, endPoint.type.simpleName, objectId, interfaceClass)
|
||||||
saveProxyObject(objectId, proxyObject)
|
saveProxyObject(objectId, proxyObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +30,19 @@ class RmiSupportConnection<C: Connection>(logger: KLogger,
|
|||||||
return proxyObject as Iface
|
return proxyObject as Iface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* on the connection+client to get a connection-specific remote object (that exists on the server/client)
|
||||||
|
*/
|
||||||
|
fun <Iface> getRemoteObject(connection: Connection, endPoint: EndPoint<*>, objectId: Int, interfaceClass: Class<Iface>): Iface {
|
||||||
|
// this immediately returns BECAUSE the object must have already been created on the server (this is why we specify the rmiId)!
|
||||||
|
return createProxyObject(false, connection, endPoint, objectId, interfaceClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* on the "client" to create a connection-specific remote object (that exists on the server)
|
* on the "client" to create a connection-specific remote object (that exists on the server)
|
||||||
*/
|
*/
|
||||||
suspend fun <Iface> createRemoteObject(connection: C, interfaceClassId: Int, callback: suspend (Iface) -> Unit) {
|
suspend fun <Iface> createRemoteObject(connection: Connection, interfaceClassId: Int, callback: suspend (Iface) -> Unit) {
|
||||||
val callbackId = rmiGlobalSupport.registerCallback(callback)
|
val callbackId = rmiGlobalSupport.registerCallback(callback)
|
||||||
|
|
||||||
// There is no rmiID yet, because we haven't created it!
|
// There is no rmiID yet, because we haven't created it!
|
||||||
@ -51,22 +58,21 @@ class RmiSupportConnection<C: Connection>(logger: KLogger,
|
|||||||
/**
|
/**
|
||||||
* called on "server"
|
* called on "server"
|
||||||
*/
|
*/
|
||||||
internal suspend fun onConnectionObjectCreateRequest(
|
internal suspend fun onConnectionObjectCreateRequest(endPoint: EndPoint<*>, connection: Connection, message: ConnectionObjectCreateRequest, logger: KLogger) {
|
||||||
endPoint: EndPoint<Connection_>, connection: Connection_, message: ConnectionObjectCreateRequest, logger: KLogger) {
|
|
||||||
|
|
||||||
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
|
val interfaceClassId = RmiUtils.unpackLeft(message.packedIds)
|
||||||
val callbackId = RmiUtils.unpackRight(message.packedIds)
|
val callbackId = RmiUtils.unpackRight(message.packedIds)
|
||||||
|
|
||||||
// We have to lookup the iface, since the proxy object requires it
|
// We have to lookup the iface, since the proxy object requires it
|
||||||
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
|
val implObject = endPoint.serialization.createRmiObject(interfaceClassId)
|
||||||
val rmiId = registerImplObject(implObject)
|
val rmiId = saveImplObject(implObject)
|
||||||
|
|
||||||
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
|
if (rmiId != RemoteObjectStorage.INVALID_RMI) {
|
||||||
// this means we could register this object.
|
// this means we could register this object.
|
||||||
|
|
||||||
// next, scan this object to see if there are any RMI fields
|
// next, scan this object to see if there are any RMI fields
|
||||||
RmiSupport.scanImplForRmiFields(logger, implObject) {
|
RmiSupport.scanImplForRmiFields(logger, implObject) {
|
||||||
registerImplObject(implObject)
|
saveImplObject(it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.error {
|
logger.error {
|
||||||
|
@ -64,8 +64,7 @@ object RmiUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (i in argTypes1.indices) {
|
for (i in argTypes1.indices) {
|
||||||
diff = argTypes1[i].name
|
diff = argTypes1[i].name.compareTo(argTypes2[i].name)
|
||||||
.compareTo(argTypes2[i].name)
|
|
||||||
if (diff != 0) {
|
if (diff != 0) {
|
||||||
return@Comparator diff
|
return@Comparator diff
|
||||||
}
|
}
|
||||||
@ -371,7 +370,7 @@ object RmiUtils {
|
|||||||
try {
|
try {
|
||||||
serializerClass.getConstructor(Class::class.java).newInstance(superClass)
|
serializerClass.getConstructor(Class::class.java).newInstance(superClass)
|
||||||
} catch (ex3: NoSuchMethodException) {
|
} catch (ex3: NoSuchMethodException) {
|
||||||
serializerClass.newInstance()
|
serializerClass.getDeclaredConstructor().newInstance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,39 +32,25 @@
|
|||||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*/
|
*/
|
||||||
package dorkbox.network.rmi;
|
package dorkbox.network.rmi
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when a method with a return value is invoked on a remote object and the response is not received with the {@link
|
* Thrown when a method with a return value is invoked on a remote object and the response is not received with the [RemoteObject.responseTimeout].
|
||||||
* RemoteObject#setResponseTimeout(int) response timeout}.
|
|
||||||
*
|
*
|
||||||
* @author Nathan Sweet <misc@n4te.com>
|
* @author Nathan Sweet
|
||||||
* @see dorkbox.network.connection.Connection#getRemoteObject(int, RemoteObjectCallback)
|
*
|
||||||
* @see dorkbox.network.connection.Connection#createRemoteObject(Class, RemoteObjectCallback)
|
* @see dorkbox.network.connection.Connection.getObject
|
||||||
|
* @see dorkbox.network.connection.Connection.createObject
|
||||||
*/
|
*/
|
||||||
public
|
class TimeoutException : IOException {
|
||||||
class TimeoutException extends IOException {
|
constructor() : super() {}
|
||||||
private static final long serialVersionUID = -3526277240277423682L;
|
constructor(message: String?, cause: Throwable?) : super(message, cause) {}
|
||||||
|
constructor(message: String?) : super(message) {}
|
||||||
|
constructor(cause: Throwable?) : super(cause) {}
|
||||||
|
|
||||||
public
|
companion object {
|
||||||
TimeoutException() {
|
private const val serialVersionUID = -3526277240277423682L
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
TimeoutException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
TimeoutException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
TimeoutException(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,3 +22,4 @@ package dorkbox.network.rmi.messages
|
|||||||
* @param callbackId (RIGHT) to know which callback to use when the object is created
|
* @param callbackId (RIGHT) to know which callback to use when the object is created
|
||||||
*/
|
*/
|
||||||
data class GlobalObjectCreateRequest(val packedIds: Int) : RmiMessage
|
data class GlobalObjectCreateRequest(val packedIds: Int) : RmiMessage
|
||||||
|
//a asd
|
||||||
|
@ -58,4 +58,9 @@ class MethodRequest : RmiMessage {
|
|||||||
|
|
||||||
// these are the arguments for executing the method (they are serialized using the info from the cachedMethod field
|
// these are the arguments for executing the method (they are serialized using the info from the cachedMethod field
|
||||||
var args: Array<Any>? = null
|
var args: Array<Any>? = null
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "MethodRequest(isGlobal=$isGlobal, objectId=$objectId, responseId=$responseId, cachedMethod=$cachedMethod, args=${args?.contentToString()})"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,9 @@ class MethodResponse : RmiMessage {
|
|||||||
|
|
||||||
// this is the result of the invoked method
|
// this is the result of the invoked method
|
||||||
var result: Any? = null
|
var result: Any? = null
|
||||||
|
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "MethodResponse(isGlobal=$isGlobal, objectId=$objectId, responseId=$responseId, result=$result)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,6 @@ class RmiClientRequestSerializer : Serializer<Any>() {
|
|||||||
kryo as KryoExtra
|
kryo as KryoExtra
|
||||||
|
|
||||||
val connection = kryo.connection
|
val connection = kryo.connection
|
||||||
return connection.endPoint().rmiSupport.getImplObject(isGlobal, objectId, connection)
|
return connection.endPoint().rmiGlobalSupport.getImplObject(isGlobal, objectId, connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy
|
|||||||
import com.esotericsoftware.kryo.util.IdentityMap
|
import com.esotericsoftware.kryo.util.IdentityMap
|
||||||
import dorkbox.network.connection.KryoExtra
|
import dorkbox.network.connection.KryoExtra
|
||||||
import dorkbox.network.connection.ping.PingMessage
|
import dorkbox.network.connection.ping.PingMessage
|
||||||
|
import dorkbox.network.handshake.Message
|
||||||
import dorkbox.network.rmi.CachedMethod
|
import dorkbox.network.rmi.CachedMethod
|
||||||
import dorkbox.network.rmi.RmiUtils
|
import dorkbox.network.rmi.RmiUtils
|
||||||
import dorkbox.network.rmi.messages.*
|
import dorkbox.network.rmi.messages.*
|
||||||
@ -93,7 +94,7 @@ class Serialization(references: Boolean,
|
|||||||
// TODO: fix kryo to work the way we want, so we can register interfaces + serializers with kryo
|
// TODO: fix kryo to work the way we want, so we can register interfaces + serializers with kryo
|
||||||
// serialization.register(XECPublicKey::class.java, XECPublicKeySerializer())
|
// serialization.register(XECPublicKey::class.java, XECPublicKeySerializer())
|
||||||
// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer())
|
// serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer())
|
||||||
serialization.register(dorkbox.network.connection.registration.Registration::class.java) // must use full package name!
|
serialization.register(Message::class.java) // must use full package name!
|
||||||
|
|
||||||
return serialization
|
return serialization
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user