287 lines
12 KiB
Kotlin
287 lines
12 KiB
Kotlin
/*
|
|
* Copyright 2020 dorkbox, llc
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package dorkbox.network.rmi
|
|
|
|
import dorkbox.network.connection.Connection
|
|
import dorkbox.network.other.SuspendWaiter
|
|
import dorkbox.network.other.invokeSuspendFunction
|
|
import dorkbox.network.rmi.messages.MethodRequest
|
|
import kotlinx.coroutines.runBlocking
|
|
import java.lang.reflect.InvocationHandler
|
|
import java.lang.reflect.Method
|
|
import java.util.*
|
|
import kotlin.coroutines.Continuation
|
|
|
|
|
|
/**
|
|
* Handles network communication when methods are invoked on a proxy. For NON-BLOCKING performance, the interface
|
|
* must have the 'suspend' keyword added. If it is not present, then all method invocation will be BLOCKING.
|
|
*
|
|
*
|
|
*
|
|
* @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 cachedMethods this is the methods available for the specified class
|
|
*/
|
|
class RmiClient(val isGlobal: Boolean,
|
|
val rmiObjectId: Int,
|
|
private val connection: Connection,
|
|
private val proxyString: String,
|
|
private val rmiSupportCache: RmiSupportCache,
|
|
private val cachedMethods: Array<CachedMethod>) : InvocationHandler {
|
|
|
|
companion object {
|
|
private val methods = RmiUtils.getMethods(RemoteObject::class.java)
|
|
|
|
private val closeMethod = methods.find { it.name == "close" }
|
|
private val setResponseTimeoutMethod = methods.find { it.name == "setResponseTimeout" }
|
|
private val getResponseTimeoutMethod = methods.find { it.name == "getResponseTimeout" }
|
|
private val setAsyncMethod = methods.find { it.name == "setAsync" }
|
|
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")
|
|
private val EMPTY_ARRAY: Array<Any> = Collections.EMPTY_LIST.toTypedArray() as Array<Any>
|
|
}
|
|
|
|
private val responseWaiter = SuspendWaiter()
|
|
|
|
private var timeoutMillis: Long = 3000
|
|
private var isAsync = false
|
|
private var allowWaiting = false
|
|
private var enableToString = false
|
|
|
|
// this is really a a short!
|
|
@Volatile
|
|
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 --
|
|
// the "server" side can have out-of-order method invocation. There are 2 ways to solve this
|
|
// 1) make the "server" side single threaded
|
|
// 2) make the "client" side wait for execution response (from the "server"). <--- this is what we are using.
|
|
//
|
|
// Because we have to ALWAYS make the client wait (unless 'isAsync' is true), we will always be returning, and will always have a
|
|
// response (even if it is a void response). This simplifies our response mask, and lets us use more bits for storing the
|
|
// response ID
|
|
|
|
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
|
|
// a thing (so we will know when to stop blocking).
|
|
val responseId = responseStorage.prep(rmiObjectId, responseWaiter)
|
|
|
|
// so we can query for async, if we want to necessary
|
|
previousResponseId = responseId
|
|
|
|
|
|
val invokeMethod = MethodRequest()
|
|
invokeMethod.isGlobal = isGlobal
|
|
invokeMethod.objectId = rmiObjectId
|
|
invokeMethod.responseId = responseId
|
|
invokeMethod.args = args
|
|
|
|
// which method do we access? We always want to access the IMPLEMENTATION (if available!). we know that this will always succeed
|
|
// this should be accessed via the KRYO class ID + method index (both are SHORT, and can be packed)
|
|
invokeMethod.cachedMethod = cachedMethods.first { it.method == method }
|
|
|
|
connection.send(invokeMethod)
|
|
|
|
// if we are async, then this will immediately return!
|
|
return try {
|
|
val result = responseStorage.waitForReply(allowWaiting, isAsync, rmiObjectId, responseId, responseWaiter, timeoutMillis)
|
|
if (result is Exception) {
|
|
throw result
|
|
} else {
|
|
result
|
|
}
|
|
} catch (ex: TimeoutException) {
|
|
throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}")
|
|
}
|
|
}
|
|
|
|
@Suppress("DuplicatedCode")
|
|
@Throws(Exception::class)
|
|
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
|
|
if (method.declaringClass == RemoteObject::class.java) {
|
|
// manage all of the RemoteObject proxy methods
|
|
when (method) {
|
|
closeMethod -> {
|
|
rmiSupportCache.removeProxyObject(rmiObjectId)
|
|
return null
|
|
}
|
|
setResponseTimeoutMethod -> {
|
|
timeoutMillis = (args!![0] as Int).toLong()
|
|
return null
|
|
}
|
|
getResponseTimeoutMethod -> {
|
|
return timeoutMillis.toInt()
|
|
}
|
|
getAsyncMethod -> {
|
|
return isAsync
|
|
}
|
|
setAsyncMethod -> {
|
|
isAsync = args!![0] as Boolean
|
|
return null
|
|
}
|
|
enableToStringMethod -> {
|
|
enableToString = args!![0] as Boolean
|
|
return null
|
|
}
|
|
getLastResponseIdMethod -> {
|
|
// only ASYNC can wait for responses
|
|
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
|
|
}
|
|
waitForLastResponseMethod -> {
|
|
// 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, 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 if (!enableToString && method == toStringMethod) {
|
|
return proxyString
|
|
}
|
|
|
|
// if a 'suspend' function is called, then our last argument is a 'Continuation' object
|
|
// We will use this for our coroutine context instead of running on a new coroutine
|
|
val maybeContinuation = args?.lastOrNull()
|
|
|
|
if (isAsync) {
|
|
// return immediately, without suspends
|
|
if (maybeContinuation is Continuation<*>) {
|
|
val argsWithoutContinuation = args.take(args.size - 1)
|
|
invokeSuspendFunction(maybeContinuation) {
|
|
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
|
|
}
|
|
} else {
|
|
runBlocking {
|
|
invokeSuspend(method, args ?: EMPTY_ARRAY)
|
|
}
|
|
}
|
|
|
|
// if we are async then we return immediately. If you want the response value, you MUST use
|
|
// 'waitForLastResponse()' or 'waitForResponse'('getLastResponseID()')
|
|
val returnType = method.returnType
|
|
if (returnType.isPrimitive) {
|
|
return when (returnType) {
|
|
Int::class.javaPrimitiveType -> {
|
|
0
|
|
}
|
|
Boolean::class.javaPrimitiveType -> {
|
|
java.lang.Boolean.FALSE
|
|
}
|
|
Float::class.javaPrimitiveType -> {
|
|
0.0f
|
|
}
|
|
Char::class.javaPrimitiveType -> {
|
|
0.toChar()
|
|
}
|
|
Long::class.javaPrimitiveType -> {
|
|
0L
|
|
}
|
|
Short::class.javaPrimitiveType -> {
|
|
0.toShort()
|
|
}
|
|
Byte::class.javaPrimitiveType -> {
|
|
0.toByte()
|
|
}
|
|
Double::class.javaPrimitiveType -> {
|
|
0.0
|
|
}
|
|
else -> {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
} else {
|
|
// non-async code, so we will be blocking/suspending!
|
|
return if (maybeContinuation is Continuation<*>) {
|
|
val argsWithoutContinuation = args.take(args.size - 1)
|
|
invokeSuspendFunction(maybeContinuation) {
|
|
invokeSuspend(method, argsWithoutContinuation.toTypedArray())
|
|
}
|
|
} else {
|
|
runBlocking {
|
|
invokeSuspend(method, args ?: EMPTY_ARRAY)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
val prime = 31
|
|
var result = 1
|
|
result = prime * result + rmiObjectId
|
|
return result
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) {
|
|
return true
|
|
}
|
|
if (other == null) {
|
|
return false
|
|
}
|
|
if (javaClass != other.javaClass) {
|
|
return false
|
|
}
|
|
|
|
if (other !is RmiClient) {
|
|
return false
|
|
}
|
|
|
|
return rmiObjectId == other.rmiObjectId
|
|
}
|
|
}
|