diff --git a/build.gradle.kts b/build.gradle.kts index eacef52a..ae2c110a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -283,6 +283,9 @@ tasks.withType { jvmTarget = Extras.JAVA_VERSION apiVersion = Extras.KOTLIN_API_VERSION languageVersion = Extras.KOTLIN_LANG_VERSION + + // enable the use of inline classes. see https://kotlinlang.org/docs/reference/inline-classes.html + freeCompilerArgs += "-Xinline-classes" } } @@ -345,11 +348,11 @@ dependencies { implementation("com.dorkbox:ObjectPool:2.12") implementation("com.dorkbox:Utilities:1.5.3") + + // https://github.com/MicroUtils/kotlin-logging implementation("io.github.microutils:kotlin-logging:1.7.9") // slick kotlin wrapper for slf4j implementation("org.slf4j:slf4j-api:1.7.30") - - testImplementation("junit:junit:4.13") testImplementation("ch.qos.logback:logback-classic:1.2.3") } diff --git a/src/dorkbox/network/other/discovery/Broadcast.java b/ignored_for_now/discovery/Broadcast.java similarity index 100% rename from src/dorkbox/network/other/discovery/Broadcast.java rename to ignored_for_now/discovery/Broadcast.java diff --git a/src/dorkbox/network/other/discovery/MagicBytes.java b/ignored_for_now/discovery/MagicBytes.java similarity index 100% rename from src/dorkbox/network/other/discovery/MagicBytes.java rename to ignored_for_now/discovery/MagicBytes.java diff --git a/src/dorkbox/network/other/discovery/server/BroadcastResponse.java b/ignored_for_now/discovery/server/BroadcastResponse.java similarity index 100% rename from src/dorkbox/network/other/discovery/server/BroadcastResponse.java rename to ignored_for_now/discovery/server/BroadcastResponse.java diff --git a/src/dorkbox/network/other/discovery/server/BroadcastServer.java b/ignored_for_now/discovery/server/BroadcastServer.java similarity index 100% rename from src/dorkbox/network/other/discovery/server/BroadcastServer.java rename to ignored_for_now/discovery/server/BroadcastServer.java diff --git a/src/dorkbox/network/other/discovery/server/ClientDiscoverHostHandler.java b/ignored_for_now/discovery/server/ClientDiscoverHostHandler.java similarity index 100% rename from src/dorkbox/network/other/discovery/server/ClientDiscoverHostHandler.java rename to ignored_for_now/discovery/server/ClientDiscoverHostHandler.java diff --git a/src/dorkbox/network/other/discovery/server/ClientDiscoverHostInitializer.java b/ignored_for_now/discovery/server/ClientDiscoverHostInitializer.java similarity index 100% rename from src/dorkbox/network/other/discovery/server/ClientDiscoverHostInitializer.java rename to ignored_for_now/discovery/server/ClientDiscoverHostInitializer.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java b/ignored_for_now/remote/RegistrationRemoteHandler.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandler.java rename to ignored_for_now/remote/RegistrationRemoteHandler.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java b/ignored_for_now/remote/RegistrationRemoteHandlerClient.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClient.java rename to ignored_for_now/remote/RegistrationRemoteHandlerClient.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java b/ignored_for_now/remote/RegistrationRemoteHandlerClientTCP.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientTCP.java rename to ignored_for_now/remote/RegistrationRemoteHandlerClientTCP.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java b/ignored_for_now/remote/RegistrationRemoteHandlerClientUDP.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerClientUDP.java rename to ignored_for_now/remote/RegistrationRemoteHandlerClientUDP.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java b/ignored_for_now/remote/RegistrationRemoteHandlerServer.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServer.java rename to ignored_for_now/remote/RegistrationRemoteHandlerServer.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java b/ignored_for_now/remote/RegistrationRemoteHandlerServerTCP.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerTCP.java rename to ignored_for_now/remote/RegistrationRemoteHandlerServerTCP.java diff --git a/src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java b/ignored_for_now/remote/RegistrationRemoteHandlerServerUDP.java similarity index 100% rename from src/dorkbox/network/connection/registration/remote/RegistrationRemoteHandlerServerUDP.java rename to ignored_for_now/remote/RegistrationRemoteHandlerServerUDP.java diff --git a/src/dorkbox/network/Client.kt b/src/dorkbox/network/Client.kt index da3533c2..8a1aee8b 100644 --- a/src/dorkbox/network/Client.kt +++ b/src/dorkbox/network/Client.kt @@ -18,6 +18,8 @@ package dorkbox.network import dorkbox.network.aeron.client.ClientException import dorkbox.network.aeron.client.ClientTimedOutException import dorkbox.network.connection.* +import dorkbox.network.other.NetUtil +import dorkbox.network.other.NetworkUtil import dorkbox.util.exceptions.SecurityException import kotlinx.atomicfu.atomic import kotlinx.coroutines.launch @@ -138,7 +140,7 @@ open class Client(config: Configuration = Configuration()) : End */ @JvmOverloads @Throws(IOException::class, ClientTimedOutException::class) - suspend fun connect(remoteAddress: String = "localhost", connectionTimeoutMS: Long = 30_000L, reliable: Boolean = true) { + suspend fun connect(remoteAddress: String = "", connectionTimeoutMS: Long = 30_000L, reliable: Boolean = true) { if (isConnected.value) { throw IOException("Unable to connect when already connected!"); } @@ -166,26 +168,52 @@ open class Client(config: Configuration = Configuration()) : End throw IllegalArgumentException("0.0.0.0 is an invalid address to connect to!") } + connectionManager.init(this) - // this is an IPC address - if (this.remoteAddress.startsWith("0x")) { - val ipcAddress: Long - try { - ipcAddress = remoteAddress.toLong(radix = 16) - } catch (e: Exception) { - throw IOException(e) - } + if (this.remoteAddress.isEmpty()) { + // this is an IPC address - // val connectionType3 = ConnectionType(config.remoteAddress, config.controlPort, config.port, false, MediaDriverType.IPC, IPC_STREAM_ID) + // When conducting IPC transfers, we MUST use the same aeron configuration as the server! +// config.aeronLogDirectory + + + + + // stream IDs are flipped for a client because we operate from the perspective of the server + val handshakeConnection = IpcMediaDriverConnection( + streamId = IPC_HANDSHAKE_STREAM_ID_SUB, + streamIdSubscription = IPC_HANDSHAKE_STREAM_ID_PUB, + sessionId = RESERVED_SESSION_ID_INVALID + ) + + closables.add(handshakeConnection) + + + // throws a ConnectTimedOutException if the client cannot connect for any reason to the server handshake ports + handshakeConnection.buildClient(aeron) +// logger.debug(handshakeConnection.clientInfo()) + + + println("CONASD") + + // this will block until the connection timeout, and throw an exception if we were unable to connect with the server + + // @Throws(ConnectTimedOutException::class, ClientRejectedException::class) + val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS) + println("CO23232232323NASD") } else { - // this is a network address + // THIS IS A NETWORK ADDRESS - // initially we only connect to the client connect ports. Ports are flipped because they are in the perspective of the SERVER + // initially we only connect to the handshake connect ports. Ports are flipped because they are in the perspective of the SERVER val handshakeConnection = UdpMediaDriverConnection( - this.remoteAddress, config.publicationPort, config.subscriptionPort, - UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID, - connectionTimeoutMS, reliable) + address = this.remoteAddress, + subscriptionPort = config.publicationPort, + publicationPort = config.subscriptionPort, + streamId = UDP_HANDSHAKE_STREAM_ID, + sessionId = RESERVED_SESSION_ID_INVALID, + connectionTimeoutMS = connectionTimeoutMS, + isReliable = reliable) closables.add(handshakeConnection) @@ -197,12 +225,18 @@ open class Client(config: Configuration = Configuration()) : End // this will block until the connection timeout, and throw an exception if we were unable to connect with the server // @Throws(ConnectTimedOutException::class, ClientRejectedException::class) - val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS, this@Client) + val connectionInfo = connectionManager.initHandshake(handshakeConnection, connectionTimeoutMS) // we are now connected, so we can connect to the NEW client-specific ports - val reliableClientConnection = UdpMediaDriverConnection(handshakeConnection.address, connectionInfo.subscriptionPort, connectionInfo.publicationPort, - connectionInfo.streamId, connectionInfo.sessionId, connectionTimeoutMS, handshakeConnection.isReliable) + val reliableClientConnection = UdpMediaDriverConnection( + address = handshakeConnection.address, + subscriptionPort = connectionInfo.subscriptionPort, + publicationPort = connectionInfo.publicationPort, + streamId = connectionInfo.streamId, + sessionId = connectionInfo.sessionId, + connectionTimeoutMS = connectionTimeoutMS, + isReliable = handshakeConnection.isReliable) // VALIDATE:: check to see if the remote connection's public key has changed! if (!validateRemoteAddress(NetworkUtil.IP.toInt(this.remoteAddress), connectionInfo.publicKey)) { @@ -308,77 +342,48 @@ open class Client(config: Configuration = Configuration()) : End // } /** - * Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map" - * to an object that is created remotely. - * + * Tells the remote connection to create a new global object that implements the specified interface. * + * The methods on the returned object will remotely execute on the (remotely) created object * The callback will be notified when the remote object has been created. * - * - * - * - * Methods that return a value will throw [TimeoutException] if the response is not received with the - * [response timeout][RemoteObject.setResponseTimeout]. - * - * - * If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must - * not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a - * void return value can be called on the update thread. - * + * If you want to create a connection specific remote object, call [Connection.create(Int, RemoteObjectCallback)] on a connection + * The callback will be notified when the remote object has been created. * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. * * - * If one wishes to change the default behavior, cast the object to access the different methods. - * ie: `RemoteObject remoteObject = (RemoteObject) test;` + * If one wishes to change the remote object behavior, cast the object [RemoteObject] to access the different methods, for example: + * ie: `val remoteObject = test as RemoteObject` * * @see RemoteObject */ -// override fun createRemoteObject(interfaceClass: Class, callback: RemoteObjectCallback) { -// try { -// connection!!.createRemoteObject(interfaceClass, callback) -// } catch (e: NullPointerException) { -// logger.error("Error creating remote object!", e) -// } -// } + suspend inline fun createObject(noinline callback: suspend (Iface) -> Unit) { + val classId = serialization.getClassId(Iface::class.java) + rmiSupport.createGlobalRemoteObject(getConnection(), classId, callback) + } /** - * Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map" - * to an object that is created remotely. + * Gets a global remote object via the ID. * + * Global remote objects are accessible to ALL connections, where as a connection specific remote object is only accessible/visible + * to the connection. * + * If you want to access a connection specific remote object, call [Connection.get(Int, RemoteObjectCallback)] on a connection * The callback will be notified when the remote object has been created. * - * - * - * - * Methods that return a value will throw [TimeoutException] if the response is not received with the - * [response timeout][RemoteObject.setResponseTimeout]. - * - * - * If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must - * not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a - * void return value can be called on the update thread. - * - * * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side * will have the proxy object replaced with the registered (non-proxy) object. * - * - * If one wishes to change the default behavior, cast the object to access the different methods. - * ie: `RemoteObject remoteObject = (RemoteObject) test;` + * If one wishes to change the remote object behavior, cast the object to a [RemoteObject] to access the different methods, for example: + * ie: `val remoteObject = test as RemoteObject` * * @see RemoteObject */ -// override fun getRemoteObject(objectId: Int, callback: RemoteObjectCallback) { -// try { -// connection!!.getRemoteObject(objectId, callback) -// } catch (e: NullPointerException) { -// logger.error("Error getting remote object!", e) -// } -// } - + fun getObject(objectId: Int, interfaceClass: Class): Iface { + return rmiSupport.getGlobalRemoteObject(getConnection(), this, objectId, interfaceClass) + } /** * Fetches the connection used by the client. @@ -389,14 +394,9 @@ open class Client(config: Configuration = Configuration()) : End * * This is preferred to [EndPoint.getConnections], as it properly does some error checking */ - // can =just use super.get connection? -// override var connection: C = TODO() -// get() = field -// set(connection) { -// super.connection = connection -// } - - + fun getConnection(): C { + return connection as C + } @Throws(ClientException::class) suspend fun send(message: Any) { diff --git a/src/dorkbox/network/Server.kt b/src/dorkbox/network/Server.kt index 77e688f7..c372d1b4 100644 --- a/src/dorkbox/network/Server.kt +++ b/src/dorkbox/network/Server.kt @@ -16,14 +16,13 @@ package dorkbox.network import dorkbox.network.aeron.server.ServerException -import dorkbox.network.connection.Connection -import dorkbox.network.connection.ConnectionManagerServer -import dorkbox.network.connection.EndPoint -import dorkbox.network.connection.UdpMediaDriverConnection +import dorkbox.network.connection.* import dorkbox.network.connection.connectionType.ConnectionProperties import dorkbox.network.connection.connectionType.ConnectionRule import dorkbox.network.ipFilter.IpFilterRule import dorkbox.network.ipFilter.IpFilterRuleType +import dorkbox.network.other.NetUtil +import dorkbox.network.other.NetworkUtil import io.aeron.FragmentAssembler import io.aeron.logbuffer.FragmentHandler import io.aeron.logbuffer.Header @@ -146,8 +145,11 @@ open class Server(config: ServerConfiguration = ServerConfigurat // The is how clients then get the new ports to connect to + other configuration options val handshakeDriver = UdpMediaDriverConnection( - config.listenIpAddress, config.subscriptionPort, config.publicationPort, - UDP_HANDSHAKE_STREAM_ID, RESERVED_SESSION_ID_INVALID) + address = config.listenIpAddress, + subscriptionPort = config.subscriptionPort, + publicationPort = config.publicationPort, + streamId = UDP_HANDSHAKE_STREAM_ID, + sessionId = RESERVED_SESSION_ID_INVALID) handshakeDriver.buildServer(aeron) @@ -155,7 +157,19 @@ open class Server(config: ServerConfiguration = ServerConfigurat val handshakeSubscription = handshakeDriver.subscription logger.debug(handshakeDriver.serverInfo()) - logger.debug("Server listening for incomming clients on ${handshakePublication.localSocketAddresses()}") + logger.debug("Server listening for incoming clients on ${handshakePublication.localSocketAddresses()}") + + + val ipcHandshakeDriver = IpcMediaDriverConnection( + streamId = IPC_HANDSHAKE_STREAM_ID_PUB, + streamIdSubscription = IPC_HANDSHAKE_STREAM_ID_SUB, + sessionId = RESERVED_SESSION_ID_INVALID + ) + ipcHandshakeDriver.buildServer(aeron) + + val ipcHandshakePublication = ipcHandshakeDriver.publication + val ipcHandshakeSubscription = ipcHandshakeDriver.subscription + /** * Note: @@ -171,17 +185,29 @@ open class Server(config: ServerConfiguration = ServerConfigurat connectionManager.receiveHandshakeMessageServer(handshakePublication, buffer, offset, length, header, this@Server) } }) + val ipcInitialConnectionHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> + actionDispatch.launch { + println("GOT MESSAGE!") + } + }) actionDispatch.launch { val pollIdleStrategy = config.pollIdleStrategy try { + var pollCount: Int while (!isShutdown()) { + pollCount = 0 + // this checks to see if there are NEW clients - var pollCount = handshakeSubscription.poll(initialConnectionHandler, 100) +// pollCount += handshakeSubscription.poll(initialConnectionHandler, 100) + + // this checks to see if there are NEW clients via IPC + pollCount += ipcHandshakeSubscription.poll(ipcInitialConnectionHandler, 100) // this manages existing clients (for cleanup + connection polling) - pollCount += connectionManager.poll() +// pollCount += connectionManager.poll() + // 0 means we idle. >0 means reset and don't idle (because there are likely more poll events) pollIdleStrategy.idle(pollCount) @@ -189,6 +215,9 @@ open class Server(config: ServerConfiguration = ServerConfigurat } finally { handshakePublication.close() handshakeSubscription.close() + + ipcHandshakePublication.close() + ipcHandshakeSubscription.close() } } @@ -210,7 +239,7 @@ open class Server(config: ServerConfiguration = ServerConfigurat * If there is nothing added to this list - then ALL are permitted */ fun addIpFilterRule(vararg rules: IpFilterRule?) { - ipFilterRules.addAll(Arrays.asList(*rules)) + ipFilterRules.addAll(listOf(*rules)) } /** @@ -299,6 +328,35 @@ open class Server(config: ServerConfiguration = ServerConfigurat // connectionManager.removeListenerManager(connection) // } + + +// +// +// /** +// * Creates a "global" remote object for use by multiple connections. +// * +// * @return the ID assigned to this RMI object +// */ +// fun create(objectId: Short, globalObject: T) { +// return rmiGlobalObjects.register(globalObject) +// } +// +// /** +// * Creates a "global" remote object for use by multiple connections. +// * +// * @return the ID assigned to this RMI object +// */ +// fun create(`object`: T): Short { +// return rmiGlobalObjects.register(`object`) ?: 0 +// } +// +// +// + + + + + /** * Adds a custom connection to the server. * diff --git a/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt index 8e4bc0e5..40d575a7 100644 --- a/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt +++ b/src/dorkbox/network/aeron/CoroutineBackoffIdleStrategy.kt @@ -326,6 +326,13 @@ class CoroutineBackoffIdleStrategy : BackoffIdleStrategyData, CoroutineIdleStrat return ALIAS } + /** + * Creates a clone of this IdleStrategy + */ + override fun clone(): CoroutineBackoffIdleStrategy { + return CoroutineBackoffIdleStrategy(maxSpins = maxSpins, maxYields = maxYields, minParkPeriodMs = minParkPeriodMs, maxParkPeriodMs = maxParkPeriodMs) + } + override fun toString(): String { return "BackoffIdleStrategy{" + "alias=" + ALIAS + diff --git a/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt index cec63c4e..fd4c08cb 100644 --- a/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt +++ b/src/dorkbox/network/aeron/CoroutineIdleStrategy.kt @@ -102,4 +102,9 @@ interface CoroutineIdleStrategy { fun alias(): String { return "" } + + /** + * Creates a clone of this IdleStrategy + */ + fun clone(): CoroutineIdleStrategy } diff --git a/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt b/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt index 80b12294..6af55ecf 100644 --- a/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt +++ b/src/dorkbox/network/aeron/CoroutineSleepingMillisIdleStrategy.kt @@ -91,6 +91,14 @@ class CoroutineSleepingMillisIdleStrategy : CoroutineIdleStrategy { return ALIAS } + /** + * Creates a clone of this IdleStrategy + */ + override fun clone(): CoroutineSleepingMillisIdleStrategy { + return CoroutineSleepingMillisIdleStrategy(sleepPeriodMs = sleepPeriodMs) + } + + override fun toString(): String { return "SleepingMillisIdleStrategy{" + "alias=" + ALIAS + diff --git a/src/dorkbox/network/connection/ClientConnectionInfo.kt b/src/dorkbox/network/connection/ClientConnectionInfo.kt index 1e2796c4..0f519dde 100644 --- a/src/dorkbox/network/connection/ClientConnectionInfo.kt +++ b/src/dorkbox/network/connection/ClientConnectionInfo.kt @@ -2,11 +2,11 @@ package dorkbox.network.connection import org.slf4j.Logger -data class ClientConnectionInfo(val subscriptionPort: Int, - val publicationPort: Int, - val sessionId: Int, - val streamId: Int, - val publicKey: ByteArray) { +class ClientConnectionInfo(val subscriptionPort: Int, + val publicationPort: Int, + val sessionId: Int, + val streamId: Int, + val publicKey: ByteArray) { fun log(handshakeSessionId: Int, logger: Logger) { logger.debug("[{}] connect {} {} (encrypted {})", handshakeSessionId, subscriptionPort, publicationPort, sessionId) diff --git a/src/dorkbox/network/connection/Connection.kt b/src/dorkbox/network/connection/Connection.kt index 1e19c3d4..a557d64a 100644 --- a/src/dorkbox/network/connection/Connection.kt +++ b/src/dorkbox/network/connection/Connection.kt @@ -15,8 +15,6 @@ */ package dorkbox.network.connection -import dorkbox.network.rmi.RemoteObjectCallback - interface Connection : AutoCloseable { /** * Has the remote ECC public key changed. This can be useful if specific actions are necessary when the key has changed. @@ -121,16 +119,6 @@ interface Connection : AutoCloseable { override fun close() - - // TODO: below should just be "new()" to create a new object, to mirror "new Object()" - // // RMI - // // client.get(5) -> gets from the server connection, if exists, then global. - // // on server, a connection local RMI object "uses" an id for global, so there will never be a conflict - // // using some tricks, we can make it so that it DOESN'T matter the order in which objects are created, - // // and can specify, if we want, the object created. - // // Once created though, as NEW ONE with the same ID cannot be created until the old one is removed! - - /** * Tells the remote connection to create a new proxy object that implements the specified interface. The methods on this object "map" * to an object that is created remotely. @@ -158,7 +146,7 @@ interface Connection : AutoCloseable { * * @see RemoteObject */ - suspend fun createRemoteObject(interfaceClass: Class, callback: RemoteObjectCallback) + suspend fun createObject(callback: suspend (Iface) -> Unit) /** * Tells the remote connection to access an already created proxy object that implements the specified interface. The methods on this object "map" @@ -188,5 +176,5 @@ interface Connection : AutoCloseable { * * @see RemoteObject */ - suspend fun getRemoteObject(objectId: Int, callback: RemoteObjectCallback) + fun getObject(objectId: Int, interfaceClass: Class): Iface } diff --git a/src/dorkbox/network/connection/ConnectionImpl.kt b/src/dorkbox/network/connection/ConnectionImpl.kt index dfe2bb1b..fe794edc 100644 --- a/src/dorkbox/network/connection/ConnectionImpl.kt +++ b/src/dorkbox/network/connection/ConnectionImpl.kt @@ -15,12 +15,12 @@ */ package dorkbox.network.connection -import dorkbox.network.NetworkUtil import dorkbox.network.Server import dorkbox.network.connection.ping.PingFuture import dorkbox.network.connection.ping.PingMessage -import dorkbox.network.rmi.ConnectionRmiSupport -import dorkbox.network.rmi.RemoteObjectCallback +import dorkbox.network.other.NetworkUtil +import dorkbox.network.rmi.RmiSupportConnection +import dorkbox.util.classes.ClassHelper import io.aeron.FragmentAssembler import io.aeron.Publication import io.aeron.Subscription @@ -33,7 +33,6 @@ import org.agrona.BitUtil import org.agrona.BufferUtil import org.agrona.DirectBuffer import org.agrona.concurrent.UnsafeBuffer -import org.slf4j.Logger import java.io.IOException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -57,10 +56,13 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi final override val sessionId: Int + private val serialization = endPoint.config.serialization + private val sendIdleStrategy = endPoint.config.sendIdleStrategy.clone() - val expirationTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong()) + private val expirationTime = System.currentTimeMillis() + + TimeUnit.SECONDS.toMillis(endPoint.config.connectionCleanupTimeoutInSeconds.toLong()) - private val logger: Logger = endPoint.logger + private val logger = endPoint.logger // private val needsLock = AtomicBoolean(false) // private val writeSignalNeeded = AtomicBoolean(false) @@ -76,8 +78,8 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi private var pingFuture: PingFuture? = null // used to store connection local listeners (instead of global listeners). Only possible on the server. - @Volatile - private var localListenerManager: ConnectionManager<*>? = null +// @Volatile +// private var localListenerManager: ConnectionManager<*>? = null // while on the CLIENT, if the SERVER's ecc key has changed, the client will abort and show an error. private var remoteKeyChanged = false @@ -91,7 +93,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi private var closeLatch: CountDownLatch? = null // RMI support for this connection - var rmiSupport: ConnectionRmiSupport + private val rmiSupportConnection: RmiSupportConnection var messageHandler: FragmentAssembler @@ -119,7 +121,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi publicationPort = mediaDriverConnection.publicationPort remoteAddress = mediaDriverConnection.address remoteAddressInt = NetworkUtil.IP.toInt(remoteAddress) - streamId = mediaDriverConnection.streamId + streamId = mediaDriverConnection.streamId // NOTE: this is UNIQUE per server! sessionId = mediaDriverConnection.sessionId messageHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> @@ -130,7 +132,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi } }) - rmiSupport = ConnectionRmiSupport(endPoint.rmiGlobalBridge) + rmiSupportConnection = RmiSupportConnection(logger, endPoint.rmiSupport, endPoint.serialization, endPoint.actionDispatch) // when closing this connection, HOW MANY endpoints need to be closed? closeLatch = CountDownLatch(1) @@ -139,8 +141,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi /** * @param now The current time * - * @return `true` if this duologue has no subscribers and the current - * time `now` is after the intended expiry date of the duologue + * @return `true` if this connection has no subscribers and the current time `now` is after the expriation date */ override fun isExpired(now: Long): Boolean { return subscription.imageCount() == 0 && now > expirationTime @@ -290,7 +291,42 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi * Safely sends objects to a destination (such as a custom object or a standard ping). */ override suspend fun send(message: Any) { - endPoint.writeMessage(publication, this, message) + // The sessionId is globally unique, and is assigned by the server. + logger.debug("[{}] send: {}", publication.sessionId(), message) + + val kryo: KryoExtra = serialization.takeKryo() + try { + kryo.write(this, message) + + val buffer = kryo.writerBuffer + val objectSize = buffer.position() + val internalBuffer = buffer.internalBuffer + + var result: Long + while (true) { + result = publication.offer(internalBuffer, 0, objectSize) + // success! + if (result > 0) { + return + } + + if (result == Publication.BACK_PRESSURED || result == Publication.ADMIN_ACTION) { + // we should retry. + sendIdleStrategy.idle() + continue + } + + // more critical error sending the message. we shouldn't retry or anything. + logger.error("Error sending message. ${EndPoint.errorCodeName(result)}") + + return + } + } catch (e: Exception) { + logger.error("Error serializing message $message", e) + } finally { + sendIdleStrategy.reset() + serialization.returnKryo(kryo) + } } @@ -359,9 +395,10 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi // remove all RMI listeners - rmiSupport.close() +// rmiSupport.close() // TODO } + // @Throws(Exception::class) // override fun exceptionCaught(context: ChannelHandlerContext, cause: Throwable) { // val channel = context.channel() @@ -500,7 +537,7 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi * This includes all proxy listeners */ override fun removeAll(): Listeners { - rmiSupport.removeAllListeners() +// rmiSupport.removeAllListeners() // TODO if (endPoint is Server) { // when we are a server, NORMALLY listeners are added at the GLOBAL level @@ -600,15 +637,18 @@ open class ConnectionImpl(val endPoint: EndPoint<*>, mediaDriverConnection: Medi // RMI methods // // - override fun rmiSupport(): ConnectionRmiSupport { - return rmiSupport + override fun rmiSupport(): RmiSupportConnection { + return rmiSupportConnection } - override suspend fun createRemoteObject(interfaceClass: Class, callback: RemoteObjectCallback) { - rmiSupport.createRemoteObject(this, interfaceClass, callback) + override suspend fun createObject(callback: suspend (Iface) -> Unit) { + val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(Function2::class.java, callback.javaClass, 0) + val interfaceClassId = endPoint.serialization.getClassId(iFaceClass) + rmiSupportConnection.createRemoteObject(this, interfaceClassId, callback) } - override suspend fun getRemoteObject(objectId: Int, callback: RemoteObjectCallback) { - rmiSupport.getRemoteObject(this, objectId, callback) + override fun getObject(objectId: Int, interfaceClass: Class): Iface { + @Suppress("UNCHECKED_CAST") + return rmiSupportConnection.getRemoteObject(this, endPoint as EndPoint, objectId, interfaceClass) } } diff --git a/src/dorkbox/network/connection/ConnectionManagerClient.kt b/src/dorkbox/network/connection/ConnectionManagerClient.kt index 627eef13..3bd2633c 100644 --- a/src/dorkbox/network/connection/ConnectionManagerClient.kt +++ b/src/dorkbox/network/connection/ConnectionManagerClient.kt @@ -22,12 +22,15 @@ class ConnectionManagerClient(logger: Logger, config: Configurat private var failed = false + lateinit var handler: FragmentHandler + lateinit var endPoint: EndPoint var sessionId: Int = 0 - @Throws(ClientTimedOutException::class, ClientRejectedException::class) - suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long, endPoint: EndPoint) : ClientConnectionInfo { - // now we have a bi-directional connection with the server on the handshake socket. - val handler: FragmentHandler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> + fun init(endPoint: EndPoint) { + this.endPoint = endPoint + + // now we have a bi-directional connection with the server on the handshake "socket". + handler = FragmentAssembler(FragmentHandler { buffer: DirectBuffer, offset: Int, length: Int, header: Header -> endPoint.actionDispatch.launch { val message = endPoint.readHandshakeMessage(buffer, offset, length, header) logger.debug("[{}] response: {}", sessionId, message) @@ -54,19 +57,25 @@ class ConnectionManagerClient(logger: Logger, config: Configurat subscriptionPort = message.publicationPort, publicationPort = message.subscriptionPort, sessionId = oneTimePad xor message.oneTimePad, - streamId = oneTimePad xor message.streamId, + streamId = oneTimePad xor message.streamId, publicKey = message.publicKey!!) connectionInfo!!.log(sessionId, logger) } }) + } - - val registrationMessage = Registration.hello(oneTimePad, config.settingsStore.getPublicKey()!!) + @Throws(ClientTimedOutException::class, ClientRejectedException::class) + suspend fun initHandshake(mediaConnection: MediaDriverConnection, connectionTimeoutMS: Long) : ClientConnectionInfo { + val registrationMessage = Registration.hello( + oneTimePad = oneTimePad, + publicKey = config.settingsStore.getPublicKey()!!, + registrationData = config.serialization.getKryoRegistrationDetails() + ) // Send the one-time pad to the server. - endPoint.writeMessage(mediaConnection.publication, registrationMessage) + endPoint.writeHandshakeMessage(mediaConnection.publication, registrationMessage) sessionId = mediaConnection.publication.sessionId() diff --git a/src/dorkbox/network/connection/ConnectionManagerServer.kt b/src/dorkbox/network/connection/ConnectionManagerServer.kt index dcb53284..f1ce129b 100644 --- a/src/dorkbox/network/connection/ConnectionManagerServer.kt +++ b/src/dorkbox/network/connection/ConnectionManagerServer.kt @@ -1,6 +1,5 @@ package dorkbox.network.connection -import dorkbox.network.NetworkUtil import dorkbox.network.ServerConfiguration import dorkbox.network.aeron.client.ClientRejectedException import dorkbox.network.aeron.server.AllocationException @@ -8,6 +7,7 @@ import dorkbox.network.aeron.server.PortAllocator import dorkbox.network.aeron.server.RandomIdAllocator import dorkbox.network.aeron.server.ServerException import dorkbox.network.connection.registration.Registration +import dorkbox.network.other.NetworkUtil import io.aeron.Image import io.aeron.Publication import io.aeron.logbuffer.Header @@ -46,37 +46,36 @@ class ConnectionManagerServer(logger: Logger, suspend fun receiveHandshakeMessageServer(handshakePublication: Publication, buffer: DirectBuffer, offset: Int, length: Int, header: Header, endPoint: EndPoint) { + // TODO: notify error callbacks if there is an exception! // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. + // For the handshake, the sessionId IS NOT GLOBALLY UNIQUE val sessionId = header.sessionId() // note: this address will ALWAYS be an IP:PORT combo val remoteIpAndPort = (header.context() as Image).sourceIdentity() + // split + val splitPoint = remoteIpAndPort.lastIndexOf(':') + val clientAddressString = remoteIpAndPort.substring(0, splitPoint) +// val port = remoteIpAndPort.substring(splitPoint+1) + val clientAddress = NetworkUtil.IP.toInt(clientAddressString) + + config as ServerConfiguration + + val message = endPoint.readHandshakeMessage(buffer, offset, length, header) + + // VALIDATE:: a Registration object is the only acceptable message during the connection phase + if (message !is Registration) { + endPoint.writeHandshakeMessage(handshakePublication, Registration.error("Invalid connection request")) + return + } + try { - // split - val splitPoint = remoteIpAndPort.lastIndexOf(':') - val clientAddressString = remoteIpAndPort.substring(0, splitPoint) -// val port = remoteIpAndPort.substring(splitPoint+1) - - val clientAddress = NetworkUtil.IP.toInt(clientAddressString) - - // TODO: notify error if there is an exceptoin! - val message = endPoint.readHandshakeMessage(buffer, offset, length, header) - logger.debug("[{}] received: {}", sessionId, message) - - config as ServerConfiguration - - // VALIDATE:: a Registration object is the only acceptable message during the connection phase - if (message !is Registration) { - endPoint.writeMessage(handshakePublication, Registration.error("Invalid connection request")) - return - } - // VALIDATE:: Check to see if there are already too many clients connected. if (connectionCount() >= config.maxClientCount) { logger.debug("server is full") - endPoint.writeMessage(handshakePublication, Registration.error("server full. Max allowed is ${config.maxClientCount}")) + endPoint.writeHandshakeMessage(handshakePublication, Registration.error("server full. Max allowed is ${config.maxClientCount}")) return } @@ -87,8 +86,12 @@ class ConnectionManagerServer(logger: Logger, return } - // VALIDATE TODO: make sure the serialization matches between the client/server! - + // VALIDATE:: make sure the serialization matches between the client/server! + if (!config.serialization.verifyKryoRegistration(message.registrationData!!)) { + // TODO: this should provide info to a callback + println("connection not allowed! registration data mismatch") + return + } // VALIDATE:: we are now connected to the client and are going to create a new connection. val currentCountForIp = connectionsPerIpCounts.getAndIncrement(clientAddress) @@ -97,109 +100,82 @@ class ConnectionManagerServer(logger: Logger, connectionsPerIpCounts.getAndDecrement(clientAddress) logger.debug("too many connections for IP address") - endPoint.writeMessage(handshakePublication, Registration.error("too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}")) + endPoint.writeHandshakeMessage(handshakePublication, Registration.error("too many connections for IP address. Max allowed is ${config.maxConnectionsPerIpAddress}")) return } + } catch (e: Exception) { + logger.error("could not validate client message: ", e) + } + + // VALIDATE:: TODO: ?? check to see if this session is ALREADY connected??. It should not be! - // VALIDATE:: TODO: ?? check to see if this session is ALREADY connected??. It should not be! + ///// + ///// + ///// DONE WITH VALIDATION + ///// + ///// - ///// - ///// - ///// DONE WITH VALIDATION - ///// - ///// + // allocate ports for the client + val connectionPorts: IntArray + + try { + // throws exception if this is not possible + connectionPorts = portAllocator.allocate(portsPerClient) + } catch (e: IllegalArgumentException) { + // have to unwind actions! + connectionsPerIpCounts.getAndDecrement(clientAddress) + + logger.error("Unable to allocate $portsPerClient ports for client connection!") + return + } + + // allocate session/stream id's + val connectionSessionId: Int + try { + connectionSessionId = sessionIdAllocator.allocate() + } catch (e: AllocationException) { + // have to unwind actions! + connectionsPerIpCounts.getAndDecrement(clientAddress) + portAllocator.free(connectionPorts) + + logger.error("Unable to allocate a session ID for the client connection!") + return + } - // allocate ports for the client - val connectionPorts: IntArray + val connectionStreamId: Int + try { + connectionStreamId = streamIdAllocator.allocate() + } catch (e: AllocationException) { + // have to unwind actions! + connectionsPerIpCounts.getAndDecrement(clientAddress) + portAllocator.free(connectionPorts) + sessionIdAllocator.free(connectionSessionId) - try { - // throws exception if this is not possible - connectionPorts = portAllocator.allocate(portsPerClient) - } catch (e: IllegalArgumentException) { - // have to unwind actions! - connectionsPerIpCounts.getAndDecrement(clientAddress) + logger.error("Unable to allocate a stream ID for the client connection!") + return + } - logger.error("Unable to allocate $portsPerClient ports for client connection!") - return - } - - // allocate session/stream id's - val connectionSessionId: Int - try { - connectionSessionId = sessionIdAllocator.allocate() - } catch (e: AllocationException) { - // have to unwind actions! - connectionsPerIpCounts.getAndDecrement(clientAddress) - portAllocator.free(connectionPorts) - - logger.error("Unable to allocate a session ID for the client connection!") - return - } + val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box? + val subscriptionPort = connectionPorts[0] + val publicationPort = connectionPorts[1] - val connectionStreamId: Int - try { - connectionStreamId = streamIdAllocator.allocate() - } catch (e: AllocationException) { - // have to unwind actions! - connectionsPerIpCounts.getAndDecrement(clientAddress) - portAllocator.free(connectionPorts) - sessionIdAllocator.free(connectionSessionId) + // create a new connection. The session ID is encrypted. + try { + // connection timeout of 0 doesn't matter. it is not used by the server + val clientConnection = UdpMediaDriverConnection( + serverAddress, subscriptionPort, publicationPort, + connectionStreamId, connectionSessionId, 0, message.isReliable) - logger.error("Unable to allocate a stream ID for the client connection!") - return - } + val connection: Connection = endPoint.newConnection(endPoint, clientConnection) - val serverAddress = config.listenIpAddress // TODO :: my IP address?? this should be the IP of the box? - val subscriptionPort = connectionPorts[0] - val publicationPort = connectionPorts[1] - - - // create a new connection. The session ID is encrypted. - try { - // connection timeout of 0 doesn't matter. it is not used by the server - val clientConnection = UdpMediaDriverConnection( - serverAddress, subscriptionPort, publicationPort, - connectionStreamId, connectionSessionId, 0, message.isReliable) - - val connection: Connection = endPoint.newConnection(endPoint, clientConnection) - - // VALIDATE:: are we allowed to connect to this server (now that we have the initial server information) - @Suppress("UNCHECKED_CAST") - val permitConnection = notifyFilter(connection as C) - if (!permitConnection) { - // have to unwind actions! - connectionsPerIpCounts.getAndDecrement(clientAddress) - portAllocator.free(connectionPorts) - sessionIdAllocator.free(connectionSessionId) - streamIdAllocator.free(connectionStreamId) - - logger.error("Error creating new duologue") - - notifyError(connection, ClientRejectedException("Connection was not permitted!")) - return - } - - logger.info("Client connected [$clientAddressString:$subscriptionPort|$publicationPort] (session: $sessionId") - logger.debug("[{}] created new client connection", connectionSessionId) - - // The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is! - val successMessage = Registration.helloAck(message.oneTimePad xor connectionSessionId) - successMessage.sessionId = sessionId // has to be the same as before (the client expects this) - successMessage.streamId = message.oneTimePad xor connectionStreamId - - successMessage.subscriptionPort = subscriptionPort - successMessage.publicationPort = publicationPort - successMessage.publicKey = config.settingsStore.getPublicKey() - - endPoint.writeMessage(handshakePublication, successMessage) - - addConnection(connection) - notifyConnect(connection) - } catch (e: Exception) { + // VALIDATE:: are we allowed to connect to this server (now that we have the initial server information) + @Suppress("UNCHECKED_CAST") + val permitConnection = notifyFilter(connection as C) + if (!permitConnection) { // have to unwind actions! connectionsPerIpCounts.getAndDecrement(clientAddress) portAllocator.free(connectionPorts) @@ -208,11 +184,37 @@ class ConnectionManagerServer(logger: Logger, logger.error("Error creating new duologue") - logger.error("could not process client message: $message") - notifyError(e) + notifyError(connection, ClientRejectedException("Connection was not permitted!")) + return } + + logger.info("Client connected [$clientAddressString:$subscriptionPort|$publicationPort] (session: $sessionId") + logger.debug("[{}] created new client connection", connectionSessionId) + + // The one-time pad is used to encrypt the session ID, so that ONLY the correct client knows what it is! + val successMessage = Registration.helloAck(message.oneTimePad xor connectionSessionId) + successMessage.sessionId = sessionId // has to be the same as before (the client expects this) + successMessage.streamId = message.oneTimePad xor connectionStreamId + + successMessage.subscriptionPort = subscriptionPort + successMessage.publicationPort = publicationPort + successMessage.publicKey = config.settingsStore.getPublicKey() + + endPoint.writeHandshakeMessage(handshakePublication, successMessage) + + addConnection(connection) + notifyConnect(connection) } catch (e: Exception) { - logger.error("could not process client message: ", e) + // have to unwind actions! + connectionsPerIpCounts.getAndDecrement(clientAddress) + portAllocator.free(connectionPorts) + sessionIdAllocator.free(connectionSessionId) + streamIdAllocator.free(connectionStreamId) + + logger.error("Error creating new duologue") + + logger.error("could not process client message: $message") + notifyError(e) } } diff --git a/src/dorkbox/network/connection/Connection_.kt b/src/dorkbox/network/connection/Connection_.kt index 78d99d85..52365b83 100644 --- a/src/dorkbox/network/connection/Connection_.kt +++ b/src/dorkbox/network/connection/Connection_.kt @@ -15,7 +15,7 @@ */ package dorkbox.network.connection -import dorkbox.network.rmi.ConnectionRmiSupport +import dorkbox.network.rmi.RmiSupportConnection import javax.crypto.SecretKey /** @@ -25,7 +25,7 @@ interface Connection_ : Connection { /** * @return the RMI support for this connection */ - fun rmiSupport(): ConnectionRmiSupport + fun rmiSupport(): RmiSupportConnection /** * This is the per-message sequence number. diff --git a/src/dorkbox/network/connection/EndPoint.kt b/src/dorkbox/network/connection/EndPoint.kt index 35eb1c81..8712b8ad 100644 --- a/src/dorkbox/network/connection/EndPoint.kt +++ b/src/dorkbox/network/connection/EndPoint.kt @@ -15,14 +15,17 @@ */ package dorkbox.network.connection -import dorkbox.network.* +import dorkbox.network.Client +import dorkbox.network.Configuration +import dorkbox.network.Server +import dorkbox.network.ServerConfiguration import dorkbox.network.aeron.CoroutineIdleStrategy import dorkbox.network.connection.ping.PingMessage import dorkbox.network.other.CryptoEccNative -import dorkbox.network.rmi.RmiServer +import dorkbox.network.other.NetworkUtil +import dorkbox.network.rmi.RmiSupport import dorkbox.network.rmi.messages.RmiMessage import dorkbox.network.serialization.NetworkSerializationManager -import dorkbox.network.serialization.Serialization import dorkbox.network.store.NullSettingsStore import dorkbox.network.store.SettingsStore import dorkbox.util.NamedThreadFactory @@ -38,9 +41,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import mu.KLogger +import mu.KotlinLogging import org.agrona.DirectBuffer -import org.slf4j.Logger -import org.slf4j.LoggerFactory import java.io.File import java.security.KeyFactory import java.security.PrivateKey @@ -58,7 +61,12 @@ import java.util.concurrent.CountDownLatch // Usually it's with ISPs. /** * represents the base of a client/server end point for interacting with aeron - */ + * + * @param type this is either "Client" or "Server", depending on who is creating this endpoint. + * @param config these are the specific connection options + * + * @throws SecurityException if unable to initialize/generate ECC keys +*/ abstract class EndPoint internal constructor(val type: Class<*>, internal val config: Configuration) : AutoCloseable { protected constructor(config: Configuration) : this(Client::class.java, config) @@ -81,7 +89,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A const val RESERVED_SESSION_ID_HIGH = Integer.MAX_VALUE const val UDP_HANDSHAKE_STREAM_ID: Int = 0x1337cafe - const val IPC_STREAM_ID: Int = 0x1337c0de + const val IPC_HANDSHAKE_STREAM_ID_PUB: Int = 0x1337c0de + const val IPC_HANDSHAKE_STREAM_ID_SUB: Int = 0x1337c0d3 init { println("THIS IS ONLY IPV4 AT THE MOMENT. IPV6 is in progress!") @@ -100,17 +109,16 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A } - // the simple name (for the class) of this connection endpoint - val name = type.simpleName - val logger: Logger = LoggerFactory.getLogger(name) + val logger: KLogger = KotlinLogging.logger(type.simpleName) internal val closables = CopyOnWriteArrayList() internal val actionDispatch = CoroutineScope(Dispatchers.Default) internal abstract val connectionManager: ConnectionManager - internal lateinit var mediaDriver: MediaDriver - internal lateinit var aeron: Aeron + internal val mediaDriverContext: MediaDriver.Context + internal val mediaDriver: MediaDriver + internal val aeron: Aeron /** * Returns the serialization wrapper if there is an object type that needs to be added outside of the basics. @@ -130,9 +138,11 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A // we only want one instance of these created. These will be called appropriately val settingsStore: SettingsStore - val rmiGlobalBridge = RmiServer(logger, true) + var disableRemoteKeyValidation = false + val rmiSupport = RmiSupport(logger, actionDispatch, config.serialization) + /** * Checks to see if this client has connected yet or not. * @@ -142,14 +152,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A */ abstract fun isConnected(): Boolean - /** - * @param type this is either "Client" or "Server", depending on who is creating this endpoint. - * @param config these are the specific connection options - * - * @throws SecurityException if unable to initialize/generate ECC keys - * @throws ServerException if unable to validate configuration - * - */ + init { // Aeron configuration @@ -173,8 +176,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A // OS.isMacOsX() -> // } -// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max") -// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max") +// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max") +// val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max") } @@ -186,8 +189,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A // OS.isMacOsX() -> // } -// val rmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.rmem_max") -// val wmem_max = dorkbox.network.NetUtil.sysctlGetInt("net.core.wmem_max") +// val rmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.rmem_max") +// val wmem_max = dorkbox.network.other.NetUtil.sysctlGetInt("net.core.wmem_max") } @@ -230,63 +233,58 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A val aeronLogDirectory = File(baseFile, baseName) if (aeronLogDirectory.exists()) { logger.error("Aeron log directory already exists! This might not be what you want!") - // avoid a collision - // aeronLogDirectory = File(baseFile, baseName + RandomUtil.get().nextInt(1000)) } logger.debug("Aeron log directory: $aeronLogDirectory") config.aeronLogDirectory = aeronLogDirectory } - // the RmiNoOpConnection must have an endpoint, and we DO NOT want it to actually setup/configure aeron! - if (config.publicationPort > 0) { - val threadFactory = NamedThreadFactory("Aeron", false) + val threadFactory = NamedThreadFactory("Aeron", false) - // LOW-LATENCY SETTINGS - // .termBufferSparseFile(false) - // .useWindowsHighResTimer(true) - // .threadingMode(ThreadingMode.DEDICATED) - // .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE) - // .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE) - // .senderIdleStrategy(NoOpIdleStrategy.INSTANCE); - val mediaDriverContext = MediaDriver.Context() - .publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW) - .publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH) - .dirDeleteOnStart(true) // TODO: FOR NOW? - .dirDeleteOnShutdown(true) - .conductorThreadFactory(threadFactory) - .receiverThreadFactory(threadFactory) - .senderThreadFactory(threadFactory) - .sharedNetworkThreadFactory(threadFactory) - .sharedThreadFactory(threadFactory) - .threadingMode(config.threadingMode) - .mtuLength(config.networkMtuSize) - .socketSndbufLength(config.sendBufferSize) - .socketRcvbufLength(config.receiveBufferSize) - .aeronDirectoryName(config.aeronLogDirectory!!.absolutePath) + // LOW-LATENCY SETTINGS + // .termBufferSparseFile(false) + // .useWindowsHighResTimer(true) + // .threadingMode(ThreadingMode.DEDICATED) + // .conductorIdleStrategy(BusySpinIdleStrategy.INSTANCE) + // .receiverIdleStrategy(NoOpIdleStrategy.INSTANCE) + // .senderIdleStrategy(NoOpIdleStrategy.INSTANCE); + mediaDriverContext = MediaDriver.Context() + .publicationReservedSessionIdLow(RESERVED_SESSION_ID_LOW) + .publicationReservedSessionIdHigh(RESERVED_SESSION_ID_HIGH) + .dirDeleteOnStart(true) // TODO: FOR NOW? + .dirDeleteOnShutdown(true) + .conductorThreadFactory(threadFactory) + .receiverThreadFactory(threadFactory) + .senderThreadFactory(threadFactory) + .sharedNetworkThreadFactory(threadFactory) + .sharedThreadFactory(threadFactory) + .threadingMode(config.threadingMode) + .mtuLength(config.networkMtuSize) + .socketSndbufLength(config.sendBufferSize) + .socketRcvbufLength(config.receiveBufferSize) + .aeronDirectoryName(config.aeronLogDirectory!!.absolutePath) - val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName()) + val aeronContext = Aeron.Context().aeronDirectoryName(mediaDriverContext.aeronDirectoryName()) - try { - mediaDriver = MediaDriver.launch(mediaDriverContext) - } catch (e: Exception) { - throw e - } - - try { - aeron = Aeron.connect(aeronContext) - } catch (e: Exception) { - try { - mediaDriver.close() - } catch (secondaryException: Exception) { - e.addSuppressed(secondaryException) - } - throw e - } - - closables.add(aeron) - closables.add(mediaDriver) + try { + mediaDriver = MediaDriver.launch(mediaDriverContext) + } catch (e: Exception) { + throw e } + try { + aeron = Aeron.connect(aeronContext) + } catch (e: Exception) { + try { + mediaDriver.close() + } catch (secondaryException: Exception) { + e.addSuppressed(secondaryException) + } + throw e + } + + closables.add(aeron) + closables.add(mediaDriver) + // serialization stuff serialization = config.serialization @@ -417,6 +415,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A */ @Suppress("MemberVisibilityCanBePrivate") open fun newConnection(endPoint: EndPoint, mediaDriverConnection: MediaDriverConnection): C { + @Suppress("UNCHECKED_CAST") return ConnectionImpl(endPoint, mediaDriverConnection) as C } @@ -498,56 +497,13 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A connectionManager.forEachConnectionDoRead(function) } - /** - * Creates a "global" RMI object for use by multiple connections. - * - * @return the ID assigned to this RMI object - */ - fun createGlobalObject(globalObject: T): Int { - return rmiGlobalBridge.register(globalObject) ?: 0 - } - - /** - * Gets a previously created "global" RMI object - * - * @param objectRmiId the ID of the RMI object to get - * - * @return null if the object doesn't exist or the ID is invalid. - */ - fun getGlobalObject(objectRmiId: Int): T { - @Suppress("UNCHECKED_CAST") - return rmiGlobalBridge.getRegisteredObject(objectRmiId) as T - } - - - - - - - - - - - - - - - - - - - suspend fun writeMessage(publication: Publication, message: Any) { - writeMessage(publication, Serialization.NOP_CONNECTION, message) - } - - suspend fun writeMessage(publication: Publication, connection: Connection_, message: Any) { - sendIdleStrategy.reset() - // TODO: WE MIGHT NOT WANT TO USE SESSIONID()!! + internal suspend fun writeHandshakeMessage(publication: Publication, message: Any) { + // The sessionId is globally unique, and is assigned by the server. logger.debug("[{}] send: {}", publication.sessionId(), message) val kryo: KryoExtra = serialization.takeKryo() try { - kryo.write(connection, message) + kryo.write(message) val buffer = kryo.writerBuffer val objectSize = buffer.position() @@ -569,28 +525,17 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A // more critical error sending the message. we shouldn't retry or anything. logger.error("Error sending message. ${errorCodeName(result)}") + return } } catch (e: Exception) { logger.error("Error serializing message $message", e) } finally { + sendIdleStrategy.reset() serialization.returnKryo(kryo) } } - - - - - - - - - - - - - /** * @param buffer The buffer * @param offset The offset from the start of the buffer @@ -598,14 +543,12 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A * * @return A string */ - fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? { - // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. -// val sessionId = header.sessionId() - // TODO: WE MIGHT NOT WANT TO USE SESSIONID()!! - + internal fun readHandshakeMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header): Any? { val kryo: KryoExtra = serialization.takeKryo() try { - return kryo.read(buffer, offset, length, Serialization.NOP_CONNECTION) + val message = kryo.read(buffer, offset, length) + logger.debug("[{}] received: {}", header.sessionId(), message) + return message } catch (e: Exception) { logger.error("Error de-serializing message on connection ${header.sessionId()}!", e) } finally { @@ -616,7 +559,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A } suspend fun readMessage(buffer: DirectBuffer, offset: Int, length: Int, header: Header, connection: Connection_) { - // The sessionId is unique within a Subscription and unique across all Publication's from a sourceIdentity. + // The sessionId is globally unique, and is assigned by the server. val sessionId = header.sessionId() // note: this address will ALWAYS be an IP:PORT combo @@ -630,7 +573,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A // val ipAsInt = NetworkUtil.IP.toInt(ip) - // TODO: WE MIGHT NOT WANT TO USE SESSIONID()!! var message: Any? = null val kryo: KryoExtra = serialization.takeKryo() @@ -638,7 +580,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A message = kryo.read(buffer, offset, length, connection) logger.debug("[{}] received: {}", sessionId, message) } catch (e: Exception) { - logger.error("[${header.sessionId()}] Error de-serializing message", e) + logger.error("[${sessionId}] Error de-serializing message", e) } finally { serialization.returnKryo(kryo) } @@ -647,7 +589,6 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A val data = ByteArray(length) buffer.getBytes(offset, data) - when (message) { is PingMessage -> { // the ping listener (internal use only!) @@ -664,8 +605,8 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A is RmiMessage -> { // if we are an RMI message/registration, we have very specific, defined behavior. // We do not use the "normal" listener callback pattern because this require special functionality - // note: RMI messages are NEVER subclassed! - connection.rmiSupport().manage(connection, message, logger) + @Suppress("UNCHECKED_CAST") + rmiSupport.manage(this as EndPoint, connection, message, logger) } is Any -> { connectionManager.notifyOnMessage(connection, message) @@ -697,7 +638,7 @@ internal constructor(val type: Class<*>, internal val config: Configuration) : A override fun toString(): String { - return "EndPoint [$name]" + return "EndPoint [${type.simpleName}]" } override fun hashCode(): Int { diff --git a/src/dorkbox/network/connection/IpcMediaDriverConnection.kt b/src/dorkbox/network/connection/IpcMediaDriverConnection.kt deleted file mode 100644 index 1d544838..00000000 --- a/src/dorkbox/network/connection/IpcMediaDriverConnection.kt +++ /dev/null @@ -1,65 +0,0 @@ -package dorkbox.network.connection - -import dorkbox.network.aeron.client.ClientTimedOutException -import io.aeron.Aeron -import io.aeron.ChannelUriStringBuilder -import io.aeron.Publication -import io.aeron.Subscription - -class IpcMediaDriverConnection(override val address: String, - override val subscriptionPort: Int, - override val publicationPort: Int, - override val streamId: Int, - override val sessionId: Int, - private val connectionTimeoutMS: Long, - override val isReliable: Boolean) : MediaDriverConnection { - - override lateinit var subscription: Subscription - override lateinit var publication: Publication - - init { - - } - - fun subscriptionURI(): ChannelUriStringBuilder { - return ChannelUriStringBuilder() - .media("ipc") - .controlMode("dynamic") - } - - // Create a publication at the given address and port, using the given stream ID. - fun publicationURI(): ChannelUriStringBuilder { - return ChannelUriStringBuilder() - .media("ipc") - } - - - @Throws(ClientTimedOutException::class) - override suspend fun buildClient(aeron: Aeron) { - - } - - override fun buildServer(aeron: Aeron) { - - } - - override fun clientInfo() : String { - return "" - } - - override fun serverInfo() : String { - return "" - } - - fun connect() : Pair { - return Pair("","") - } - - override fun close() { - - } - - override fun toString(): String { - return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId]" - } -} diff --git a/src/dorkbox/network/connection/KryoExtra.kt b/src/dorkbox/network/connection/KryoExtra.kt index 2a922b35..c11ba160 100644 --- a/src/dorkbox/network/connection/KryoExtra.kt +++ b/src/dorkbox/network/connection/KryoExtra.kt @@ -20,8 +20,7 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import dorkbox.network.pipeline.AeronInput import dorkbox.network.pipeline.AeronOutput -import dorkbox.network.rmi.ConnectionRmiSupport -import dorkbox.network.serialization.NetworkSerializationManager +import dorkbox.network.rmi.CachedMethod import dorkbox.util.OS import dorkbox.util.bytes.OptimizeUtilsByteArray import dorkbox.util.bytes.OptimizeUtilsByteBuf @@ -29,15 +28,15 @@ import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBufUtil import net.jpountz.lz4.LZ4Factory import org.agrona.DirectBuffer +import org.agrona.collections.Int2ObjectHashMap import org.slf4j.Logger import java.io.IOException -import java.security.SecureRandom import javax.crypto.Cipher /** * Nothing in this class is thread safe */ -class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() { +class KryoExtra(private val methodCache: Int2ObjectHashMap>) : Kryo() { // for kryo serialization private val readerBuffer = AeronInput() val writerBuffer = AeronOutput() @@ -50,10 +49,9 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() // volatile to provide object visibility for entire class. This is unique per connection - lateinit var rmiSupport: ConnectionRmiSupport lateinit var connection: Connection_ - private val secureRandom = SecureRandom() +// private val secureRandom = SecureRandom() private var cipher: Cipher? = null private val compressor = factory.fastCompressor() private val decompressor = factory.fastDecompressor() @@ -81,6 +79,24 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() } } + fun getMethods(classId: Int): Array { + return methodCache[classId] + } + + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun write(message: Any) { + writerBuffer.reset() + writeClassAndObject(writerBuffer, message) + } + /** * OUTPUT: * ++++++++++++++++++++++++++ @@ -91,12 +107,27 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() fun write(connection: Connection_, message: Any) { // required by RMI and some serializers to determine which connection wrote (or has info about) this object this.connection = connection - this.rmiSupport = connection.rmiSupport() writerBuffer.reset() writeClassAndObject(writerBuffer, message) } + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + @Throws(Exception::class) + fun read(buffer: DirectBuffer, offset: Int, length: Int): Any { + // this properly sets the buffer info + readerBuffer.setBuffer(buffer, offset, length) + + return readClassAndObject(readerBuffer) + } + /** * INPUT: * ++++++++++++++++++++++++++ @@ -107,7 +138,6 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() fun read(buffer: DirectBuffer, offset: Int, length: Int, connection: Connection_): Any { // required by RMI and some serializers to determine which connection wrote (or has info about) this object this.connection = connection - this.rmiSupport = connection.rmiSupport() // this properly sets the buffer info readerBuffer.setBuffer(buffer, offset, length) @@ -124,6 +154,20 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() //////////////// //////////////// + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * OUTPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + private fun write(writer: Output, message: Any) { + // write the object to the NORMAL output buffer! + writer.reset() + writeClassAndObject(writer, message) + } + /** * OUTPUT: * ++++++++++++++++++++++++++ @@ -133,13 +177,24 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() private fun write(connection: Connection_, writer: Output, message: Any) { // required by RMI and some serializers to determine which connection wrote (or has info about) this object this.connection = connection - this.rmiSupport = connection.rmiSupport() // write the object to the NORMAL output buffer! writer.reset() writeClassAndObject(writer, message) } + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * INPUT: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + private fun read(reader: Input): Any { + return readClassAndObject(reader) + } + /** * INPUT: * ++++++++++++++++++++++++++ @@ -149,11 +204,54 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() private fun read(connection: Connection_, reader: Input): Any { // required by RMI and some serializers to determine which connection wrote (or has info about) this object this.connection = connection - this.rmiSupport = connection.rmiSupport() return readClassAndObject(reader) } + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * BUFFER: + * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + * + uncompressed length (1-4 bytes) + compressed data + + * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + * + * COMPRESSED DATA: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + fun writeCompressed(logger: Logger, buffer: ByteBuf, message: Any) { + // write the object to a TEMP buffer! this will be compressed later + write(writer, message) + + // save off how much data the object took + val length = writer.position() + val maxCompressedLength = compressor.maxCompressedLength(length) + + ////////// compressing data + // we ALWAYS compress our data stream -- because of how AES-GCM pads data out, the small input (that would result in a larger + // output), will be negated by the increase in size by the encryption + val compressOutput = temp + + // LZ4 compress. + val compressedLength = compressor.compress(writer.buffer, 0, length, compressOutput, 0, maxCompressedLength) + if (DEBUG) { + val orig = ByteBufUtil.hexDump(writer.buffer, 0, length) + val compressed = ByteBufUtil.hexDump(compressOutput, 0, compressedLength) + logger.error(OS.LINE_SEPARATOR + + "ORIG: (" + length + ")" + OS.LINE_SEPARATOR + orig + + OS.LINE_SEPARATOR + + "COMPRESSED: (" + compressedLength + ")" + OS.LINE_SEPARATOR + compressed) + } + + // now write the ORIGINAL (uncompressed) length. This is so we can use the FAST decompress version + OptimizeUtilsByteBuf.writeInt(buffer, length, true) + + // have to copy over the orig data, because we used the temp buffer. Also have to account for the length of the uncompressed size + buffer.writeBytes(compressOutput, 0, compressedLength) + } + /** * BUFFER: * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -196,6 +294,60 @@ class KryoExtra(val serializationManager: NetworkSerializationManager) : Kryo() buffer.writeBytes(compressOutput, 0, compressedLength) } + /** + * NOTE: THIS CANNOT BE USED FOR ANYTHING RELATED TO RMI! + * + * BUFFER: + * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + * + uncompressed length (1-4 bytes) + compressed data + + * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + * + * COMPRESSED DATA: + * ++++++++++++++++++++++++++ + * + class and object bytes + + * ++++++++++++++++++++++++++ + */ + fun readCompressed(logger: Logger, buffer: ByteBuf, length: Int): Any { + //////////////// + // Note: we CANNOT write BACK to the buffer as "temp" storage, since there could be additional data on it! + //////////////// + + // get the decompressed length (at the beginning of the array) + var length = length + val uncompressedLength = OptimizeUtilsByteBuf.readInt(buffer, true) + if (uncompressedLength > ABSOLUTE_MAX_SIZE_OBJECT) { + throw IOException("Uncompressed size ($uncompressedLength) is larger than max allowed size ($ABSOLUTE_MAX_SIZE_OBJECT)!") + } + + // because 1-4 bytes for the decompressed size (this number is never negative) + val lengthLength = OptimizeUtilsByteArray.intLength(uncompressedLength, true) + val start = buffer.readerIndex() + + // have to adjust for uncompressed length-length + length = length - lengthLength + + + ///////// decompress data + buffer.readBytes(temp, 0, length) + + + // LZ4 decompress, requires the size of the ORIGINAL length (because we use the FAST decompressor) + reader.reset() + decompressor.decompress(temp, 0, reader.buffer, 0, uncompressedLength) + reader.setLimit(uncompressedLength) + if (DEBUG) { + val compressed = ByteBufUtil.hexDump(buffer, start, length) + val orig = ByteBufUtil.hexDump(reader.buffer, start, uncompressedLength) + logger.error(OS.LINE_SEPARATOR + + "COMPRESSED: (" + length + ")" + OS.LINE_SEPARATOR + compressed + + OS.LINE_SEPARATOR + + "ORIG: (" + uncompressedLength + ")" + OS.LINE_SEPARATOR + orig) + } + + // read the object from the buffer. + return read(reader) + } + /** * BUFFER: * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/dorkbox/network/connection/Listener.kt b/src/dorkbox/network/connection/Listener.kt index 04f9e8ee..db2b0d5f 100644 --- a/src/dorkbox/network/connection/Listener.kt +++ b/src/dorkbox/network/connection/Listener.kt @@ -17,22 +17,6 @@ package dorkbox.network.connection interface Listener {} -/** - * Called before the remote end has been connected. - *

- * This permits the addition of connection filters to decide if a connection is permitted. - */ -interface FilterConnection { - /** - * Called before the remote end has been connected. - *

- * This permits the addition of connection filters to decide if a connection is permitted. - *

- * @return true if the connection is permitted, false if it will be rejected - */ - fun filter(connection: C): Boolean -} - /** * Called when the remote end has been connected. This will be invoked before any objects are received by the network. */ @@ -43,51 +27,4 @@ interface OnConnected { fun connected(connection: C) } -/** - * Called when the remote end is no longer connected. - *

- * Do not try to send messages! The connection will already be closed, resulting in an error if you attempt to do so. - */ -interface OnDisconnected { - /** - * Called when the remote end is no longer connected. - *

- * Do not write data in this method! The connection can already be closed, resulting in an error if you attempt to do so. - */ - fun disconnected(connection: C) -} -/** - * Called when there is an error - *

- * The error is also sent to an error log before this method is called. - */ -interface OnError { - /** - * Called when there is an error - *

- * The error is sent to an error log before this method is called. - */ - fun error(connection: C, throwable: Throwable) -} - -/** - * Called when an object has been received from the remote end of the connection. - *

- * This method should not block for long periods as other network activity will not be processed until it returns. - */ -interface OnMessageReceived { - fun received(connection: C, message: M) -} - -/** - * Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since - * the system looks up incoming message types to determine what listeners to dispatch them to. - */ -interface SelfDefinedType { - /** - * Permits a listener to specify it's own referenced object type, if passing in a generic parameter doesn't work. This is necessary since - * the system looks up incoming message types to determine what listeners to dispatch them to. - */ - fun type(): Class<*> -} diff --git a/src/dorkbox/network/connection/Listeners.kt b/src/dorkbox/network/connection/Listeners.kt index 567c5a13..9e502c88 100644 --- a/src/dorkbox/network/connection/Listeners.kt +++ b/src/dorkbox/network/connection/Listeners.kt @@ -20,7 +20,6 @@ package dorkbox.network.connection * accidentally add an incompatible connection type. */ interface Listeners where C : Connection { - /** * Adds a function that will be called BEFORE a client/server "connects" with * each other, and used to determine if a connection should be allowed diff --git a/src/dorkbox/network/connection/UdpMediaDriverConnection.kt b/src/dorkbox/network/connection/MediaDriverConnection.kt similarity index 55% rename from src/dorkbox/network/connection/UdpMediaDriverConnection.kt rename to src/dorkbox/network/connection/MediaDriverConnection.kt index 5e489d68..99001424 100644 --- a/src/dorkbox/network/connection/UdpMediaDriverConnection.kt +++ b/src/dorkbox/network/connection/MediaDriverConnection.kt @@ -35,12 +35,12 @@ interface MediaDriverConnection : AutoCloseable { * For a client, the ports specified here MUST be manually flipped because they are in the perspective of the SERVER */ class UdpMediaDriverConnection(override val address: String, - override val subscriptionPort: Int, - override val publicationPort: Int, - override val streamId: Int, - override val sessionId: Int, - private val connectionTimeoutMS: Long = 0, - override val isReliable: Boolean = true) : MediaDriverConnection { + override val subscriptionPort: Int, + override val publicationPort: Int, + override val streamId: Int, + override val sessionId: Int, + private val connectionTimeoutMS: Long = 0, + override val isReliable: Boolean = true) : MediaDriverConnection { override lateinit var subscription: Subscription override lateinit var publication: Publication @@ -174,3 +174,137 @@ class UdpMediaDriverConnection(override val address: String, return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId] (reliable:$isReliable)" } } + +/** + * For a client, the streamId specified here MUST be manually flipped because they are in the perspective of the SERVER + */ +class IpcMediaDriverConnection(override val streamId: Int, + val streamIdSubscription: Int, + override val sessionId: Int, + private val connectionTimeoutMS: Long = 30_000, + override val isReliable: Boolean = true) : MediaDriverConnection { + + override val address = "" + override val subscriptionPort = 0 + override val publicationPort = 0 + + override lateinit var subscription: Subscription + override lateinit var publication: Publication + + var success: Boolean = false + + + init { + } + + private fun uri(): ChannelUriStringBuilder { + val builder = ChannelUriStringBuilder().media("ipc") + if (sessionId != EndPoint.RESERVED_SESSION_ID_INVALID) { + builder.sessionId(sessionId) + } + + return builder + } + + @Throws(ClientTimedOutException::class) + override suspend fun buildClient(aeron: Aeron) { + // Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID. + val subscriptionUri = uri() +// .controlEndpoint("$address:$subscriptionPort") +// .controlMode("dynamic") + + + // Create a publication at the given address and port, using the given stream ID. + // Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. + val publicationUri = uri() +// .endpoint("$address:$publicationPort") + + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + val subscription = aeron.addSubscription(subscriptionUri.build(), streamIdSubscription) + val publication = aeron.addPublication(publicationUri.build(), streamId) + + var success = false + + // this will wait for the server to acknowledge the connection (all via aeron) + var startTime = System.currentTimeMillis() + while (System.currentTimeMillis() - startTime < connectionTimeoutMS) { + if (subscription.isConnected && subscription.imageCount() > 0) { + success = true + break + } + + delay(timeMillis = 10L) + } + + if (!success) { + subscription.close() + throw ClientTimedOutException("Creating subscription connection to aeron") + } + + + success = false + + // this will wait for the server to acknowledge the connection (all via aeron) + startTime = System.currentTimeMillis() + while (System.currentTimeMillis() - startTime < connectionTimeoutMS) { + if (publication.isConnected) { + success = true + break + } + + delay(timeMillis = 10L) + } + + if (!success) { + subscription.close() + publication.close() + throw ClientTimedOutException("Creating publication connection to aeron") + } + + this.success = true + + this.subscription = subscription + this.publication = publication + } + + override fun buildServer(aeron: Aeron) { + // Create a subscription with a control port (for dynamic MDC) at the given address and port, using the given stream ID. + val subscriptionUri = uri() +// .endpoint("$address:$subscriptionPort") + + + // Create a publication with a control port (for dynamic MDC) at the given address and port, using the given stream ID. + // Note: The Aeron.addPublication method will block until the Media Driver acknowledges the request or a timeout occurs. + val publicationUri = uri() +// .controlEndpoint("$address:$publicationPort") +// .controlMode("dynamic") + + + // NOTE: Handlers are called on the client conductor thread. The client conductor thread expects handlers to do safe + // publication of any state to other threads and not be long running or re-entrant with the client. + subscription = aeron.addSubscription(subscriptionUri.build(), streamIdSubscription) + publication = aeron.addPublication(publicationUri.build(), streamId) + } + + override fun clientInfo() : String { + return "" + } + + override fun serverInfo() : String { + return "" + } + + fun connect() : Pair { + return Pair("","") + } + + override fun close() { + + } + + override fun toString(): String { + return "$address [$subscriptionPort|$publicationPort] [$streamId|$sessionId]" + } +} diff --git a/src/dorkbox/network/connection/registration/Registration.kt b/src/dorkbox/network/connection/registration/Registration.kt index b239b837..db0bc9f2 100644 --- a/src/dorkbox/network/connection/registration/Registration.kt +++ b/src/dorkbox/network/connection/registration/Registration.kt @@ -25,11 +25,13 @@ class Registration private constructor() { // -1 means there is an error var state = INVALID + var errorMessage: String? = null var publicationPort = 0 var subscriptionPort = 0 var sessionId = 0 var streamId = 0 + var publicKey: ByteArray? = null // by default, this will be a reliable connection. When the client connects to the server, the client will specify if the new connection @@ -37,9 +39,12 @@ class Registration private constructor() { val isReliable = true + // the client sends it's registration data to the server to make sure that the registered classes are the same between the client/server + var registrationData: ByteArray? = null + + // NOTE: this is for ECDSA! // var eccParameters: IESParameters? = null - var payload: ByteArray? = null // > 0 when we are ready to setup the connection (hasMore will always be false if this is >0). 0 when we are ready to connect // ALSO used if there are fragmented frames for registration data (since we have to split it up to fit inside a single UDP packet without fragmentation) @@ -53,11 +58,12 @@ class Registration private constructor() { const val HELLO = 0 const val HELLO_ACK = 1 - fun hello(oneTimePad: Int, publicKey: ByteArray): Registration { + fun hello(oneTimePad: Int, publicKey: ByteArray, registrationData: ByteArray): Registration { val hello = Registration() hello.state = HELLO hello.oneTimePad = oneTimePad hello.publicKey = publicKey + hello.registrationData = registrationData return hello } diff --git a/src/dorkbox/network/other/CoroutineUtils.kt b/src/dorkbox/network/other/CoroutineUtils.kt new file mode 100644 index 00000000..fdd21c89 --- /dev/null +++ b/src/dorkbox/network/other/CoroutineUtils.kt @@ -0,0 +1,34 @@ +package dorkbox.network.other + +import java.lang.reflect.InvocationTargetException +import kotlin.coroutines.Continuation + +@FunctionalInterface +private interface SuspendFunction { + suspend fun invoke(): Any? +} + +// we access this via reflection, because we have to be able to pass the continuation object as the LAST parameter. We don't know what +// the method signature actually is, so this is necessary +private val SuspendMethod = SuspendFunction::class.java.methods[0] + +internal inline fun handleInvocationTargetException(action: () -> Any?): Any? { + return try { + action() + } catch (e: InvocationTargetException) { + throw e.cause!! + } +} + + +internal fun invokeSuspendFunction(continuation: Continuation<*>, suspendFunction: suspend () -> Any?): Any? { + return handleInvocationTargetException { + SuspendMethod.invoke( + object : SuspendFunction { + override suspend fun invoke() = suspendFunction() + }, + continuation + ) + } +} + diff --git a/src/dorkbox/network/NetUtil.java b/src/dorkbox/network/other/NetUtil.java similarity index 99% rename from src/dorkbox/network/NetUtil.java rename to src/dorkbox/network/other/NetUtil.java index df252886..e78bb62c 100644 --- a/src/dorkbox/network/NetUtil.java +++ b/src/dorkbox/network/other/NetUtil.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package dorkbox.network; +package dorkbox.network.other; import static io.netty.util.AsciiString.indexOf; diff --git a/src/dorkbox/network/NetworkUtil.java b/src/dorkbox/network/other/NetworkUtil.java similarity index 99% rename from src/dorkbox/network/NetworkUtil.java rename to src/dorkbox/network/other/NetworkUtil.java index c0a16f13..3cc8c7a4 100644 --- a/src/dorkbox/network/NetworkUtil.java +++ b/src/dorkbox/network/other/NetworkUtil.java @@ -1,4 +1,4 @@ -package dorkbox.network; +package dorkbox.network.other; import java.io.BufferedWriter; import java.io.File; diff --git a/src/dorkbox/network/other/SuspendWaiter.kt b/src/dorkbox/network/other/SuspendWaiter.kt new file mode 100644 index 00000000..1eeec056 --- /dev/null +++ b/src/dorkbox/network/other/SuspendWaiter.kt @@ -0,0 +1,14 @@ +package dorkbox.network.other + +import kotlinx.coroutines.channels.Channel + +// 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://kotlinlang.org/docs/reference/coroutines/channels.html +inline class SuspendWaiter(private val channel: Channel = Channel(0)) { + // "receive' suspends until another coroutine invokes "send" + // and + // "send" suspends until another coroutine invokes "receive". + suspend fun doWait() { channel.receive() } + suspend fun doNotify() { channel.send(Unit) } + fun cancel() { channel.cancel() } +} diff --git a/src/dorkbox/network/rmi/CachedMethod.kt b/src/dorkbox/network/rmi/CachedMethod.kt index 2524d079..e52e74dd 100644 --- a/src/dorkbox/network/rmi/CachedMethod.kt +++ b/src/dorkbox/network/rmi/CachedMethod.kt @@ -39,7 +39,7 @@ import dorkbox.network.connection.Connection import java.lang.reflect.Method /** - * This class is NOT sent across the wire + * This class is NOT sent across the wire, but some of it's contents are */ open class CachedMethod(val method: Method, val methodIndex: Int, val methodClassId: Int, val serializers: Array?>) { /** diff --git a/src/dorkbox/network/rmi/ConnectionRmiSupport.kt b/src/dorkbox/network/rmi/ConnectionRmiSupport.kt deleted file mode 100644 index a8983db9..00000000 --- a/src/dorkbox/network/rmi/ConnectionRmiSupport.kt +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright 2019 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.* -import dorkbox.network.rmi.messages.DynamicObjectRequest -import dorkbox.network.rmi.messages.DynamicObjectResponse -import dorkbox.network.rmi.messages.MethodRequest -import dorkbox.network.rmi.messages.MethodResponse -import dorkbox.network.serialization.NetworkSerializationManager -import dorkbox.util.classes.ClassHelper -import dorkbox.util.collections.LockFreeHashMap -import dorkbox.util.collections.LockFreeIntMap -import org.slf4j.Logger -import java.io.IOException -import java.lang.reflect.Proxy -import java.util.* -import java.util.concurrent.CopyOnWriteArrayList - -class ConnectionRmiSupport internal constructor(private val rmiGlobal: RmiServer) { - private val rmiLocal: RmiServer - private val proxyIdCache: MutableMap - private val proxyListeners: MutableList> - private val rmiRegistrationCallbacks: LockFreeIntMap> - - - @Volatile - private var rmiCallbackId = 0 - - init { - // * @param executor - // * Sets the executor used to invoke methods when an invocation is received from a remote endpoint. By default, no - // * executor is set and invocations occur on the network thread, which should not be blocked for long, May be null. - rmiLocal = RmiServer(rmiGlobal.logger, false) - proxyIdCache = LockFreeHashMap() - proxyListeners = CopyOnWriteArrayList() - rmiRegistrationCallbacks = LockFreeIntMap() - } - - fun close() { - // proxy listeners are cleared in the removeAll() call (which happens BEFORE close) - proxyIdCache.clear() - rmiRegistrationCallbacks.clear() - } - - /** - * This will remove the invoke and invoke response listeners for this remote object - */ - fun removeAllListeners() { - proxyListeners.clear() - } - - suspend fun createRemoteObject( - connection: ConnectionImpl, - interfaceClass: Class, - callback: RemoteObjectCallback) { - - require(interfaceClass.isInterface) { "Cannot create a proxy for RMI access. It must be an interface." } - - // because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent - // access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections - val nextRmiCallbackId = rmiCallbackId++ - rmiRegistrationCallbacks.put(nextRmiCallbackId, callback) - val message = DynamicObjectRequest(interfaceClass, RmiServer.INVALID_RMI, nextRmiCallbackId) - - // 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, bound access to only this connection - connection.send(message) - } - - suspend fun getRemoteObject(connection: ConnectionImpl, objectId: Int, callback: RemoteObjectCallback) { - - check(objectId >= 0) { "Object ID cannot be < 0" } - check(objectId < RmiServer.INVALID_RMI) { "Object ID cannot be >= " + RmiServer.INVALID_RMI } - - val iFaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0) - - // because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent - // access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections - val nextRmiCallbackId = rmiCallbackId++ - rmiRegistrationCallbacks.put(nextRmiCallbackId, callback) - val message = DynamicObjectRequest(iFaceClass, objectId, nextRmiCallbackId) - - // 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 getting an EXISTING object on the server, bound access to only this connection - connection.send(message) - } - - /** - * Manages the RMI stuff for a connection. Will register/invoke/etc on the RMI object - */ - suspend fun manage(connection: Connection_, message: Any, logger: Logger) { - when (message) { - is MethodRequest -> { - val objectID = message.objectId - - // have to make sure to get the correct object (global vs local) - // This is what is overridden when registering interfaces/classes for RMI. - // objectID is the interface ID, and this returns the implementation ID. - val target = getImplementationObject(objectID) - if (target == null) { - logger.warn("Ignoring remote invocation request for unknown object ID: {}", objectID) - return - } - - try { - val result = RmiServer.invoke(connection, target, message, logger) - if (result != null) { - // System.err.println("Sending: " + invokeMethod.responseID); - connection.send(result) - } - } catch (e: IOException) { - logger.error("Unable to invoke method.", e) - } - } - is MethodResponse -> { - for (proxyListener in proxyListeners) { - proxyListener.received(connection, message) - } - } - is DynamicObjectRequest -> { - // Check if we are creating a new REMOTE object. This check is always first. - if (message.rmiId == RmiServer.INVALID_RMI) { - // THIS IS ON THE REMOTE CONNECTION (where the object will really exist as an implementation) - // - // CREATE a new ID, and register the ID and new object (must create a new one) in the object maps - - // have to lookup the implementation class - val serialization = connection.endPoint().serialization - - // For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically. - val registrationResult = createNewRmiObject(serialization, message.interfaceClass, message.callbackId, logger) - connection.send(registrationResult) - // connection transport is flushed in calling method (don't need to do it here) - } else { - // THIS IS ON THE REMOTE CONNECTION (where the object implementation will really exist) - // - // GET a LOCAL rmi object, if none get a specific, GLOBAL rmi object (objects that are not bound to a single connection). - val implementationObject = getImplementationObject(message.rmiId) - connection.send(DynamicObjectResponse(message.interfaceClass, message.rmiId, message.callbackId, implementationObject!!)) - // connection transport is flushed in calling method (don't need to do it here) - } - } - is DynamicObjectResponse -> { - if (message.rmiId == RmiServer.INVALID_RMI) { - logger.error("RMI ID '{}' is invalid. Unable to create RMI object.", message.rmiId) - } - - // this is the response. - // THIS IS ON THE LOCAL CONNECTION SIDE, which is the side that called 'getRemoteObject()' - @Suppress("UNCHECKED_CAST") - val callback: RemoteObjectCallback = rmiRegistrationCallbacks.remove(message.callbackId) as RemoteObjectCallback - - try { - callback.created(message.remoteObject!!) - } catch (e: Exception) { - logger.error("Error getting or creating the remote object ${message.interfaceClass}", e) - } - } - } - } - - /** - * Used by RMI by the LOCAL side when setting up the to fetch an object for the REMOTE side - * - * @return the registered ID for a specific object, or RmiBridge.INVALID_RMI if there was no ID. - */ - fun getRegisteredId(`object`: T): Int { - // always check global before checking local, because less contention on the synchronization - val objectId = rmiGlobal.getRegisteredId(`object`) - return if (objectId != RmiServer.INVALID_RMI) { - objectId - } else { - // might return RmiBridge.INVALID_RMI; - rmiLocal.getRegisteredId(`object`) - } - } - - /** - * This is used by RMI for the SERVER side, to get the implementation - * - * @param objectId this is the RMI object ID - */ - fun getImplementationObject(objectId: Int): Any? { - return if (RmiServer.isGlobal(objectId)) { - rmiGlobal.getRegisteredObject(objectId) - } else { - rmiLocal.getRegisteredObject(objectId) - } - } - - /** - * Removes a proxy object from the system - */ - fun removeProxyObject(rmiProxyHandler: RmiClient) { - proxyListeners.remove(rmiProxyHandler.listener) - proxyIdCache.remove(rmiProxyHandler.rmiObjectId) - } - - /** - * For network connections, the interface class kryo ID == implementation class kryo ID, so they switch automatically. - * For local connections, we have to switch it appropriately in the LocalRmiProxy - */ - fun createNewRmiObject(serialization: NetworkSerializationManager, interfaceClass: Class<*>, callbackId: Int, logger: Logger): DynamicObjectResponse { - var kryo: KryoExtra? = null - var remoteObject: Any? = null - var rmiId = 0 - val implementationClass = serialization.getRmiImpl(interfaceClass) - - try { - kryo = serialization.takeKryo() - - // because the INTERFACE is what is registered with kryo (not the impl) we have to temporarily permit unregistered classes (which have an ID of -1) - // so we can cache the instantiator for this class. - val registrationRequired = kryo.isRegistrationRequired - kryo.isRegistrationRequired = false - - // this is what creates a new instance of the impl class, and stores it as an ID. - remoteObject = kryo.newInstance(implementationClass) - if (registrationRequired) { - // only if it's different should we call this again. - kryo.isRegistrationRequired = true - } - rmiId = rmiLocal.register(remoteObject) - - if (rmiId == RmiServer.INVALID_RMI) { - // this means that there are too many RMI ids (either global or connection specific!) - remoteObject = null - } else { - // if we are invalid, skip going over fields that might also be RMI objects, BECAUSE our object will be NULL! - - // the @Rmi annotation allows an RMI object to have fields with objects that are ALSO RMI objects - val classesToCheck = LinkedList, Any?>>() - classesToCheck.add(AbstractMap.SimpleEntry(implementationClass, remoteObject)) - - var remoteClassObject: Map.Entry, Any?> - while (!classesToCheck.isEmpty()) { - remoteClassObject = classesToCheck.removeFirst() - - // we have to check the IMPLEMENTATION for any additional fields that will have proxy information. - // we use getDeclaredFields() + walking the object hierarchy, so we get ALL the fields possible (public + private). - for (field in remoteClassObject.key.declaredFields) { - if (field.getAnnotation(Rmi::class.java) != null) { - val type = field.type - if (!type.isInterface) { - // the type must be an interface, otherwise RMI cannot create a proxy object - logger.error("Error checking RMI fields for: {}.{} -- It is not an interface!", - remoteClassObject.key, - field.name) - continue - } - - val prev = field.isAccessible - field.isAccessible = true - - val o: Any - try { - o = field[remoteClassObject.value] - rmiLocal.register(o) - classesToCheck.add(AbstractMap.SimpleEntry(type, o)) - } catch (e: IllegalAccessException) { - logger.error("Error checking RMI fields for: {}.{}", remoteClassObject.key, field.name, e) - } finally { - field.isAccessible = prev - } - } - } - - - // have to check the object hierarchy as well - val superclass = remoteClassObject.key - .superclass - if (superclass != null && superclass != Any::class.java) { - classesToCheck.add(AbstractMap.SimpleEntry(superclass, remoteClassObject.value)) - } - } - } - } catch (e: Exception) { - logger.error("Error registering RMI class $implementationClass", e) - } finally { - if (kryo != null) { - // we use kryo to create a new instance - so only return it on error or when it's done creating a new instance - serialization.returnKryo(kryo) - } - } - - return DynamicObjectResponse(interfaceClass, rmiId, callbackId, remoteObject!!) - } - - /** - * Warning. This is an advanced method. You should probably be using [Connection.createRemoteObject] - * - * - * - * - * Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked - * remotely. - * - * - * Methods that return a value will throw [TimeoutException] if the response is not received with the [ ][RemoteObject.setResponseTimeout]. - * - * - * If [non-blocking][RemoteObject.setAsync] is false (the default), then methods that return a value must not be - * called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a void return value can be - * called on the update thread. - * - * - * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side will - * have the proxy object replaced with the registered object. - * - * @see RemoteObject - * - * @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID - * @param iFace this is the RMI interface - */ - fun getProxyObject(connection: Connection_, rmiId: Int, iFace: Class<*>?): RemoteObject { - requireNotNull(iFace) { "iface cannot be null." } - require(iFace.isInterface) { "iface must be an interface." } - - // we want to have a connection specific cache of IDs - // because this is PER CONNECTION, there is no need for synchronize(), since there will not be any issues with concurrent - // access, but there WILL be issues with thread visibility because a different worker thread can be called for different connections - var remoteObject = proxyIdCache[rmiId] - if (remoteObject == null) { - // duplicates are fine, as they represent the same object (as specified by the ID) on the remote side. - - // the ACTUAL proxy is created in the connection impl. Our proxy handler MUST BE suspending because of: - // 1) how we send data on the wire - // 2) how we must (sometimes) wait for a response - val proxyObject = RmiClient(connection, this, rmiId, iFace) - proxyListeners.add(proxyObject.listener) - - // This is the interface inheritance by the proxy object - val interfaces: Array> = arrayOf(RemoteObject::class.java, iFace) - - remoteObject = Proxy.newProxyInstance(RmiServer::class.java.classLoader, interfaces, proxyObject) as RemoteObject - proxyIdCache[rmiId] = remoteObject - } - return remoteObject - } - - - - -} diff --git a/src/dorkbox/network/rmi/NopRmiConnection.kt b/src/dorkbox/network/rmi/NopRmiConnection.kt deleted file mode 100644 index 50ebeacb..00000000 --- a/src/dorkbox/network/rmi/NopRmiConnection.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2019 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 com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import dorkbox.network.Configuration -import dorkbox.network.connection.* -import dorkbox.network.serialization.NetworkSerializationManager -import dorkbox.network.store.NullSettingsStore -import io.netty.buffer.ByteBuf -import org.slf4j.LoggerFactory -import java.io.File -import javax.crypto.SecretKey - -class NopRmiConnection : Connection_ { - val logger = LoggerFactory.getLogger("RMI_NO_OP") - val rmiBridgeUnnecessaryMaybe = RmiServer(logger, true) - - val config = Configuration().apply { - aeronLogDirectory = File("") - settingsStore = NullSettingsStore() - serialization = object : NetworkSerializationManager { - override fun registerRmi(ifaceClass: Class, implClass: Class): NetworkSerializationManager { - return this - } - - override fun takeKryo(): KryoExtra { - TODO("Not yet implemented") - } - -// override fun readWithCompression(connection: Connection_, length: Int): Any { -// return false -// } - - override fun getRmiImpl(iFace: Class): Class { - TODO("Not yet implemented") - } - - override fun readFullClassAndObject(input: Input?): Any { - return false - } - -// override fun write(connection: Connection_, message: Any) { -// } - - override fun write(buffer: ByteBuf, message: Any) { - } - - override fun initialized(): Boolean { - return false - } - - override fun getMethods(classID: Int): Array { - TODO("Not yet implemented") - } - - override fun finishInit(endPointClass: Class<*>) { - } - -// override fun writeWithCompression(connection: Connection_, message: Any) { -// } - - override fun getKryoRegistrationDetails(): ByteArray { - return ByteArray(0) - } - - override fun writeFullClassAndObject(output: Output?, value: Any?) { - } - - override fun register(clazz: Class): NetworkSerializationManager { - return this - } - - override fun register(clazz: Class, id: Int): NetworkSerializationManager { - return this - } - - override fun register(clazz: Class, serializer: Serializer): NetworkSerializationManager { - return this - } - - override fun register(clazz: Class, serializer: Serializer, id: Int): NetworkSerializationManager { - return this - } - - override fun verifyKryoRegistration(bytes: ByteArray): Boolean { - return false - } - -// override fun writeWithCrypto(connection: Connection_, message: Any) { -// } - - override fun returnKryo(kryo: KryoExtra) { - } - -// override fun read(connection: Connection_, length: Int): Any { -// return false -// } - - override fun read(buffer: ByteBuf, length: Int): Any { - return false - } - -// override fun readWithCrypto(connection: Connection_, length: Int): Any { -// return false -// } - } - } - - val connectionManager2: ConnectionManager = ConnectionManager(logger, config) - val rmiEndpoint: EndPoint = object : EndPoint(Any::class.java, config) { - override val connectionManager: ConnectionManager = connectionManager2 - override fun isConnected(): Boolean { return false } - } - - val con = object : Connection_ { - override fun pollSubscriptions(): Int { - return 0 - } - - override fun rmiSupport(): ConnectionRmiSupport { - TODO("Not yet implemented") - } - - override fun nextGcmSequence(): Long { - TODO("Not yet implemented") - } - - override fun cryptoKey(): SecretKey { - TODO("Not yet implemented") - } - - override fun isExpired(now: Long): Boolean { - return false - } - - override fun isClosed(): Boolean { - return true - } - - override fun endPoint(): EndPoint<*> { - TODO("Not yet implemented") - } - - override fun hasRemoteKeyChanged(): Boolean { - TODO("Not yet implemented") - } - - override val subscriptionPort = 0 - override val publicationPort = 0 - - override val remoteAddress = "0.0.0.0" - override val remoteAddressInt = 0 - override val streamId = 0 - override val sessionId = 0 - override val isLoopback = false - - override val isIPC = false - override val isNetwork = false - - override suspend fun send(message: Any) { - TODO("Not yet implemented") - } - - override suspend fun send(message: Any, priority: Byte) { - TODO("Not yet implemented") - } - - override suspend fun ping(): Ping { - TODO("Not yet implemented") - } - - override fun close() { - TODO("Not yet implemented") - } - - override suspend fun createRemoteObject(interfaceClass: Class, callback: RemoteObjectCallback) { - TODO("Not yet implemented") - } - - override suspend fun getRemoteObject(objectId: Int, callback: RemoteObjectCallback) { - TODO("Not yet implemented") - } - } - - val rmiSUpport = ConnectionRmiSupport(rmiBridgeUnnecessaryMaybe) - - init { - println("CREATED RMI NO OP") - } - - override fun hasRemoteKeyChanged(): Boolean { - return false - } - - override val subscriptionPort = 0 - override val publicationPort = 0 - override val remoteAddress = "" - override val remoteAddressInt = 0 - - override val isLoopback = false - override val isIPC = false - override val isNetwork = false - - override fun endPoint(): EndPoint<*> { - return rmiEndpoint - } - - override suspend fun send(message: Any) {} - override suspend fun send(message: Any, priority: Byte) {} - - override suspend fun ping(): Ping { - TODO("Not yet implemented") - } - - override fun close() {} - override suspend fun createRemoteObject(interfaceClass: Class, callback: RemoteObjectCallback) {} - override suspend fun getRemoteObject(objectId: Int, callback: RemoteObjectCallback) {} - override fun nextGcmSequence(): Long { - return 0 - } - - override fun cryptoKey(): SecretKey { - TODO("Not yet implemented") - } - - override fun pollSubscriptions(): Int { - return 0 - } - - override fun isExpired(now: Long): Boolean { - return false - } - - override val streamId: Int = 0 - override val sessionId: Int = 0 - - override fun rmiSupport(): ConnectionRmiSupport { - return rmiSUpport - } - - override fun isClosed(): Boolean { - return false - } -} diff --git a/src/dorkbox/network/rmi/RemoteObject.kt b/src/dorkbox/network/rmi/RemoteObject.kt index 2508ef02..0f31330f 100644 --- a/src/dorkbox/network/rmi/RemoteObject.kt +++ b/src/dorkbox/network/rmi/RemoteObject.kt @@ -47,19 +47,23 @@ interface RemoteObject { var responseTimeout: Int /** - * Sets the blocking behavior when invoking a remote method. Default is false (blocking). - * - * @param enable If false, the invoking thread will wait for the remote method to return or timeout. - * If true, the invoking thread will not wait for a response. The method will return immediately and the return value - * should be ignored. - * - * WHEN TRUE, it will be impossible to - * - * If return values are being transmitted, the return value or any thrown exception can later be retrieved with - * [.waitForLastResponse] or [.waitForResponse(id)]. The responses will be stored until retrieved, so each method call - * should have a matching retrieve. + * @return the ID of response for the last method invocation. */ - fun setAsync(enable: Boolean) + 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 + * should be ignored. + * + * If false, the invoking thread will wait (if called via suspend, then it will use coroutines) for the remote method to return or + * timeout. + * + * The return value or any thrown exception can later be retrieved with [RemoteObject.waitForLastResponse] or [RemoteObject.waitForResponse]. + * The responses will be stored until retrieved, so each method call should have a matching retrieve. + */ + var async: Boolean /** * Permits calls to [Object.toString] to actually return the `toString()` method on the object. @@ -69,26 +73,37 @@ interface RemoteObject { */ fun enableToString(enableDetailedToString: Boolean) + /** + * Permits calls to [RemoteObject.waitForLastResponse] and [RemoteObject.waitForResponse] to actually wait for a response. + * + * You must be in ASYNC mode already 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 enableWaiting if true, you want wait for the method results. If false, undefined errors can happen while waiting + */ + fun enableWaitingForResponse(enableWaiting: Boolean) + /** * Waits for the response to the last method invocation 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 + * * @return the response of the last method invocation */ - fun waitForLastResponse(): Any? - - /** - * @return the ID of response for the last method invocation. - */ - val lastResponseID: Byte + suspend fun waitForLastResponse(): Any? /** * Waits for the specified method invocation response to be received or the response timeout to be reached. * - * @param responseID this is the response ID obtained via [.getLastResponseID] + * 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 */ - fun waitForResponse(responseID: Byte): Any? + suspend fun waitForResponse(responseId: Int): Any? /** * Causes this RemoteObject to stop listening to the connection for method invocation response messages. diff --git a/src/dorkbox/network/rmi/RemoteObjectCallback.kt b/src/dorkbox/network/rmi/RemoteObjectCallback.kt index db16e032..cc1f0c27 100644 --- a/src/dorkbox/network/rmi/RemoteObjectCallback.kt +++ b/src/dorkbox/network/rmi/RemoteObjectCallback.kt @@ -18,9 +18,10 @@ package dorkbox.network.rmi /** * Callback for creating remote RMI classes */ +@FunctionalInterface interface RemoteObjectCallback { /** * @param remoteObject the remote object (as a proxy object) or null if there was an error creating the RMI object */ - fun created(remoteObject: Iface) + suspend fun created(remoteObject: Iface) } diff --git a/src/dorkbox/network/rmi/RemoteObjectStorage.kt b/src/dorkbox/network/rmi/RemoteObjectStorage.kt new file mode 100644 index 00000000..0d0af0e6 --- /dev/null +++ b/src/dorkbox/network/rmi/RemoteObjectStorage.kt @@ -0,0 +1,286 @@ +/* + * 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.util.collections.LockFreeIntBiMap +import mu.KLogger +import org.agrona.collections.IntArrayList +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write + +/** + * This class allows you to store objects in it via an ID. + * + * + * The ID can be reserved ahead of time, or it can be dynamically generated. Additionally, this class will recycle IDs, and prevent + * reserved IDs from being dynamically selected. + * + * ADDITIONALLY, these IDs are limited to SHORT size (65535 max value) because when executing remote methods, a lot, it is important to + * have as little data overhead in the message as possible. + * + * These data structures are not SHORTs because the JVM doesn't have good support for SHORT. + * + * From https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf + * + * The Java Virtual Machine provides the most direct support for data of type int. This is partly in anticipation of efficient + * implementations of the Java Virtual Machine's operand stacks and local variable arrays. It is also motivated by the frequency of + * int data in typical programs. Other integral types have less direct support. There are no byte, char, or short versions of the + * store, load, or add instructions, for instance. + * + * + * In situations where we want to pass in the Connection (to an RMI method) as a parameter, we have to be able to override method A, + * with method B. + * + * This is to support calling RMI methods from an interface (that does pass the connection reference) to an implType, that DOES pass + * the connection reference. The remote side (that initiates the RMI calls), MUST use the interface, and the implType may override + * the method, so that we add the connection as the first in the list of parameters. + * + * + * for example: + * Interface: foo(String x) + * Impl: foo(Connection c, String x) + * + * + * The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface + * instead of the method that would NORMALLY be called. + * + * @author Nathan Robinson + */ +class RemoteObjectStorage(val logger: KLogger) { + + companion object { + const val INVALID_RMI = 0 + } + + // this is the ID -> Object RMI map. The RMI ID is used (not the kryo ID) + private val objectMap = LockFreeIntBiMap(INVALID_RMI) + + private val idLock = ReentrantReadWriteLock() + + // object ID's are assigned OR requested, so we construct the data structures differently + // there are 2 ways to get an RMI object ID + // 1) request the next number from the counter + // 2) specifically request a number + // To solve this, we use 3 data structures, because it's also possible to RETURN no-longer needed object ID's (like when a + // connection closes) + private var objectIdCounter: Int = 1 + private val reservedObjectIds = IntArrayList(1, INVALID_RMI) + private val objectIds = IntArrayList(16, INVALID_RMI) + + init { + (0..8).forEach { _ -> + objectIds.addInt(objectIdCounter++) + } + } + + private fun validate(objectId: Int) { + require(objectId > 0) { "The ID must be greater than 0" } + require(objectId <= 65535) { "The ID must be less than 65,535" } + } + + /** + * @return the next ID or 0 (INVALID_RMI, if it's invalid) + */ + private fun unsafeNextId(): Int { + val id = if (objectIds.size > 0) { + objectIds.removeAt(objectIds.size - 1) + } else { + objectIdCounter++ + } + + if (objectIdCounter > 65535) { + // basically, it's a short (but collections are a LOT easier to deal with if it's an int) + val msg = "Max ID size is 65535, because of how we pack the bytes when sending RMI messages. FATAL ERROR! (too many objects)" + logger.error(msg) + return INVALID_RMI + } + + return id + } + + /** + * @return the next possible RMI object ID. Either one that is next available, or 0 (INVALID_RMI) if it was invalid + */ + fun nextId(): Int { + idLock.write { + var idToReturn = unsafeNextId() + while (reservedObjectIds.contains(idToReturn)) { + idToReturn = unsafeNextId() + } + + return idToReturn + } + } + + + /** + * Reserves an ID so that other requests for ID's will never return this ID. The number must be > 0 and < 65535 + * + * Reservations are permanent and it will ALWAYS be reserved! You cannot "un-reserve" an ID. + * + * If you care about memory and performance, use the ID from "nextId()" instead. + * + * @return false if this ID was not able to be reserved + */ + fun reserveId(id: Int): Boolean { + validate(id) + + idLock.write { + val contains = objectIds.remove(id) + if (contains) { + // this id is available for us to use (and was temporarily used before) + return true + } + + if (reservedObjectIds.contains(id)) { + // this id is ALREADY used by something else + return false + } + + if (objectIdCounter < id) { + // this id is ALREADY used by something else + return false + } + + if (objectIdCounter == id) { + // we are available via the counter, so make sure the counter increments + objectIdCounter++ + // we still want to mark this as reserved, so fall through + } + + // this means that the counter is LARGER than the id (maybe even a LOT larger) + // we just stuff this requested number in a small array and check it whenever we get a new number + reservedObjectIds.add(id) + return true + } + } + + /** + * @return an ID to be used again. Reserved IDs will not be allowed to be returned + */ + fun returnId(id: Int) { + idLock.write { + if (reservedObjectIds.contains(id)) { + logger.error { + "Do not return a reserved ID ($id). Once an ID is reserved, it is permanent." + } + return + } + + val shortCheck: Int = (id + 1) + if (shortCheck == objectIdCounter) { + objectIdCounter-- + } else { + objectIds.add(id) + } + return + } + } + + + + + /** + * Automatically registers an object with the next available ID to allow a remote connection to access this object via the returned ID + * + * @return the RMI object ID, there are too many, it will fail with a Runtime Exception. (Max limit is 65535 objects) + */ + fun register(`object`: Any): Int { + // this will return INVALID_RMI if there are too many in the ObjectSpace + val nextObjectId = nextId() + if (nextObjectId != INVALID_RMI) { + objectMap.put(nextObjectId, `object`) + + logger.trace { + "Object registered with .toString() = '${`object`}'" + } + } + + return nextObjectId + } + + /** + * Registers an object to allow a remote connection access to this object via the specified ID + * + * @param objectId Must not be <= 0 or > 65535 + * @return true if successful, false if there was an error + */ + fun register(objectId: Int, `object`: Any): Boolean { + validate(objectId) + + objectMap.put(objectId, `object`) + + logger.trace { + "Object registered with .toString() = '${`object`}'" + } + + return true + } + + /** + * Removes an object. The remote connection will no longer be able to access it. + */ + fun remove(objectId: Int): T { + validate(objectId) + + val rmiObject = objectMap.remove(objectId) as T + returnId(objectId) + + logger.trace { + "Object removed with .toString() = '${rmiObject}'" + } + @Suppress("UNCHECKED_CAST") + return rmiObject + } + + /** + * Removes an object, and the remote end of the RmiBridge connection will no longer be able to access it. + */ + fun remove(remoteObject: Any) { + val objectId = objectMap.inverse().remove(remoteObject) + + if (objectId == INVALID_RMI) { + logger.error("Object {} could not be found in the ObjectSpace.", remoteObject) + } else { + returnId(objectId) + + logger.trace { + "Object '${remoteObject}' (ID: ${objectId}) removed from RMI system." + } + } + } + + /** + * @return the object registered with the specified ID. + */ + operator fun get(objectId: Int): Any { + validate(objectId) + + return objectMap[objectId] + } + + /** + * @return the ID registered for the specified object, or INVALID_RMI if not found. + */ + fun getId(remoteObject: T): Int { + // Find an ID with the object. + return objectMap.inverse()[remoteObject] + } + + fun close() { + objectMap.clear() + } +} diff --git a/src/dorkbox/network/rmi/RmiClient.kt b/src/dorkbox/network/rmi/RmiClient.kt index dceb53b3..b0db7ab4 100644 --- a/src/dorkbox/network/rmi/RmiClient.kt +++ b/src/dorkbox/network/rmi/RmiClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010 dorkbox, llc + * 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. @@ -12,64 +12,37 @@ * 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. */ package dorkbox.network.rmi -import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue import dorkbox.network.connection.Connection -import dorkbox.network.connection.Connection_ -import dorkbox.network.connection.KryoExtra -import dorkbox.network.connection.OnMessageReceived +import dorkbox.network.other.SuspendWaiter +import dorkbox.network.other.invokeSuspendFunction import dorkbox.network.rmi.messages.MethodRequest -import dorkbox.network.rmi.messages.MethodResponse -import kotlinx.coroutines.launch -import org.agrona.collections.IntArrayList -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.IOException +import kotlinx.coroutines.runBlocking import java.lang.reflect.InvocationHandler import java.lang.reflect.Method -import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.ReentrantLock +import java.util.* +import kotlin.coroutines.Continuation /** - * Handles network communication when methods are invoked on a proxy. + * 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. * * - * If the method return type is 'void', then we don't have to explicitly set 'transmitReturnValue' to false - * - * - * If there are no checked exceptions thrown, then we don't have to explicitly set 'transmitExceptions' to false * * @param connection this is really the network client -- there is ONLY ever 1 connection * @param rmiSupport is used to provide RMI support - * @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID - * @param iFace this is the RMI interface + * @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(private val connection: Connection_, - private val rmiSupport: ConnectionRmiSupport, // this is the RMI id +class RmiClient(val isGlobal: Boolean, val rmiObjectId: Int, - iFace: Class<*>?) : InvocationHandler { + private val connection: Connection, + private val proxyString: String, + private val rmiSupportCache: RmiSupportCache, + private val cachedMethods: Array) : InvocationHandler { companion object { private val methods = RmiUtils.getMethods(RemoteObject::class.java) @@ -78,156 +51,31 @@ class RmiClient(private val connection: Connection_, 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 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 = Collections.EMPTY_LIST.toTypedArray() as Array } - private val logger: Logger + private val responseWaiter = SuspendWaiter() - private val lock = ReentrantLock() - private val responseCondition = lock.newCondition() - - private val responseTable = arrayOfNulls(256) - private val pendingResponses = BooleanArray(256) - - - // this is the KRYO class id - private val classId: Int - private val proxyString = "" - - val listener: OnMessageReceived - private var timeoutMillis = 3000 + private var timeoutMillis: Long = 3000 private var isAsync = false - + private var allowWaiting = false private var enableToString = false - - // for responseId's, "0" means no response (or invalid response id) - private val responseIds = MultithreadConcurrentQueue(256) - - private var previousResponseId: Byte = 0x00 + // this is really a a short! + @Volatile + private var previousResponseId: Int = 0 - init { - // create a shuffled list of ID's - val ids = IntArrayList() - for (id in 1..255) { - ids.addInt(id) - } - ids.shuffle() - - // populate the array of randomly assigned ID's. - ids.forEach { - responseIds.offer(it.toByte()) - } - - // figure out the class ID for this RMI object - val endPoint = connection.endPoint() - val serializationManager = endPoint.serialization - - var kryoExtra: KryoExtra? = null - try { - kryoExtra = serializationManager.takeKryo() - classId = kryoExtra.getRegistration(iFace).id - } finally { - if (kryoExtra != null) { - serializationManager.returnKryo(kryoExtra) - } - } - - logger = LoggerFactory.getLogger(endPoint.name + ":" + this.javaClass.simpleName) - // this listener is called when the "server" responds to our request. - // SPECIFICALLY, this is "unblocks" our "waiting for response" logic - listener = object : OnMessageReceived { - override fun received(connection: Connection, message: MethodResponse) { - // we have to check object ID's, BECAUSE we can have different objects all listening for a response - if (message.objectId != rmiObjectId) { - return - } - - val responseIdAsInt = 0xFF and message.responseId.toInt() - synchronized(this) { - if (pendingResponses[responseIdAsInt]) { - responseTable[responseIdAsInt] = message - } - } - lock.lock() - try { - responseCondition.signalAll() - } finally { - lock.unlock() - } - } - } - } - - @Throws(Exception::class) - override fun invoke(proxy: Any, method: Method, args: Array?): Any? { - val declaringClass = method.declaringClass - if (declaringClass == RemoteObject::class.java) { - // manage all of the RemoteObject proxy methods - when (method) { - closeMethod -> { - rmiSupport.removeProxyObject(this) - return null - } - setResponseTimeoutMethod -> { - timeoutMillis = args!![0] as Int - return null - } - getResponseTimeoutMethod -> { - return timeoutMillis - } - setAsyncMethod -> { - isAsync = args!![0] as Boolean - return null - } - enableToStringMethod -> { - enableToString = args!![0] as Boolean - return null - } - waitForLastResponseMethod -> { - return waitForResponse(0xFF and previousResponseId.toInt()) - } - getLastResponseIDMethod -> { - return previousResponseId - } - waitForResponseMethod -> { - check(!isAsync) { "This RemoteObject is not currently set to ASYNC mode. Unable to ignore all responses." } - return waitForResponse(args!![0] as Int) - } - else -> throw Exception("Invocation handler could not find RemoteObject method for ${method.name}") - } - } else if (!enableToString && method == toStringMethod) { - return proxyString - } - - val invokeMethod = MethodRequest() - invokeMethod.objectId = rmiObjectId - invokeMethod.args = args - - // which method do we access? We always want to access the IMPLEMENTATION (if available!) - val cachedMethods = connection.endPoint().serialization.getMethods(classId) - var i = 0 - val n = cachedMethods.size - while (i < n) { - val cachedMethod = cachedMethods[i] - val checkMethod = cachedMethod.method - if (checkMethod == method) { - invokeMethod.cachedMethod = cachedMethod - break - } - i++ - } - - // a value of 0 means there is no response (for 'async' calls only) - val responseId: Byte - val responseIdAsInt: Int - val returnType = method.returnType - + private suspend fun invokeSuspend(method: Method, args: Array): 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 @@ -237,86 +85,31 @@ class RmiClient(private val connection: Connection_, // 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 always ignore the response. - // A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses - if (isAsync) { - responseId = 0x00 - responseIdAsInt = 0 - } else { - responseId = responseIds.poll() - responseIdAsInt = 0xFF and responseId.toInt() + // 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) - synchronized(this) { - pendingResponses[responseIdAsInt] = true - } - - invokeMethod.responseId = responseId - } - - // so we can query this, if necessary + // 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 - // Sends our invokeMethod to the remote connection, which the RmiBridge listens for - val endPoint = connection.endPoint() - endPoint.actionDispatch.launch { - connection.send(invokeMethod); - - if (logger.isTraceEnabled) { - var argString = args?.contentDeepToString() ?: "" - argString = argString.substring(1, argString.length - 1) - logger.trace("$connection sent: ${method.declaringClass.simpleName}#${method.name}($argString)") - } - } - -// // if a 'suspend' function is called, then our last argument is a 'Continuation' object (and we can use that instead of runBlocking) -// val continuation = args?.lastOrNull() -// if (continuation is Continuation<*>) { -//// val continuation = args!!.last() as Continuation<*> -//// val argsWithoutContinuation = args.take(args.size - 1) -//// continuation.context. -// return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -// } - - - // MUST use 'waitForLastResponse()' or 'waitForResponse'('getLastResponseID()') to get the response - // If we are async then we return immediately - // If we are 'void' return type and do not throw checked exceptions then we return immediately - if (isAsync) { - if (returnType.isPrimitive) { - if (returnType == Int::class.javaPrimitiveType) { - return 0 - } - if (returnType == Boolean::class.javaPrimitiveType) { - return java.lang.Boolean.FALSE - } - if (returnType == Float::class.javaPrimitiveType) { - return 0.0f - } - if (returnType == Char::class.javaPrimitiveType) { - return 0.toChar() - } - if (returnType == Long::class.javaPrimitiveType) { - return 0L - } - if (returnType == Short::class.javaPrimitiveType) { - return 0.toShort() - } - if (returnType == Byte::class.javaPrimitiveType) { - return 0.toByte() - } - if (returnType == Double::class.javaPrimitiveType) { - return 0.0 - } - } - return null - } + // 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 = waitForResponse(responseIdAsInt) + val result = responseStorage.waitForReply(allowWaiting, isAsync, rmiObjectId, responseId, responseWaiter, timeoutMillis) if (result is Exception) { throw result } else { @@ -324,58 +117,146 @@ class RmiClient(private val connection: Connection_, } } catch (ex: TimeoutException) { throw TimeoutException("Response timed out: ${method.declaringClass.name}.${method.name}") - } finally { - synchronized(this) { - pendingResponses[responseIdAsInt] = false - responseTable[responseIdAsInt] = null - } } } - /** - * A timeout of 0 means that we want to disable waiting, otherwise - it waits in milliseconds - */ - @Throws(IOException::class) - private fun waitForResponse(responseIdAsInt: Int): Any? { - // if timeout == 0, we wait "forever" - var remaining: Long - val endTime: Long + @Suppress("DuplicatedCode") + @Throws(Exception::class) + override fun invoke(proxy: Any, method: Method, args: Array?): 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" } - if (timeoutMillis != 0) { - remaining = timeoutMillis.toLong() - endTime = System.currentTimeMillis() + remaining - } else { - // not forever, but close enough - remaining = Long.MAX_VALUE - endTime = Long.MAX_VALUE + 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 } - // wait for the specified time - var methodResponse: MethodResponse? - while (remaining > 0) { - synchronized(this) { - methodResponse = responseTable[responseIdAsInt] - } + // 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 (methodResponse != null) { - previousResponseId = 0 // 0 is "no response" or "invalid" - return methodResponse!!.result + if (isAsync) { + // return immediately, without suspends + if (maybeContinuation is Continuation<*>) { + val argsWithoutContinuation = args.take(args.size - 1) + invokeSuspendFunction(maybeContinuation) { + invokeSuspend(method, argsWithoutContinuation.toTypedArray()) + } } else { - lock.lock() - try { - responseCondition.await(remaining, TimeUnit.MILLISECONDS) - } catch (e: InterruptedException) { - Thread.currentThread() - .interrupt() - throw IOException("Response timed out.", e) - } finally { - lock.unlock() + 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) } } - remaining = endTime - System.currentTimeMillis() } - throw TimeoutException("Response timed out.") } override fun hashCode(): Int { @@ -395,7 +276,11 @@ class RmiClient(private val connection: Connection_, if (javaClass != other.javaClass) { return false } - val other1 = other as RmiClient - return rmiObjectId == other1.rmiObjectId + + if (other !is RmiClient) { + return false + } + + return rmiObjectId == other.rmiObjectId } } diff --git a/src/dorkbox/network/rmi/RmiResponseStorage.kt b/src/dorkbox/network/rmi/RmiResponseStorage.kt new file mode 100644 index 00000000..cadce380 --- /dev/null +++ b/src/dorkbox/network/rmi/RmiResponseStorage.kt @@ -0,0 +1,145 @@ +package dorkbox.network.rmi + +import com.conversantmedia.util.concurrent.MultithreadConcurrentQueue +import dorkbox.network.other.SuspendWaiter +import dorkbox.network.rmi.messages.MethodResponse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.agrona.collections.Hashing +import org.agrona.collections.Int2NullableObjectHashMap +import org.agrona.collections.IntArrayList +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * Manages the "pending response" from method invocation. + * + * response ID's and the memory they hold will leak if the response never arrives! + */ +class RmiResponseStorage(private val actionDispatch: CoroutineScope) { + // 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 stored here as int, however these are REALLY shorts and are int-packed when transferring data on the wire + private val rmiResponseIds = MultithreadConcurrentQueue(65535) + + private val pendingLock = ReentrantReadWriteLock() + private val pending = Int2NullableObjectHashMap(32, Hashing.DEFAULT_LOAD_FACTOR, true) + + init { + // create a shuffled list of ID's. This operation is ONLY performed ONE TIME per endpoint! + val ids = IntArrayList() + for (id in Short.MIN_VALUE..Short.MAX_VALUE) { + ids.addInt(id) + } + ids.shuffle() + + // populate the array of randomly assigned ID's. + ids.forEach { + rmiResponseIds.offer(it) + } + } + + + // resume any pending remote object method invocations (if they are not async, or not manually waiting) + suspend fun onMessage(message: MethodResponse) { + val objectId = message.objectId + val responseId = message.responseId + val result = message.result + + val pendingId = RmiUtils.packShorts(objectId, responseId) + + val previous = pendingLock.write { pending.put(pendingId, result) } + + // if NULL, since either we don't exist, or it was cancelled + if (previous is SuspendWaiter) { + // this means we were NOT timed out! + previous.doNotify() + } + + // always return the responseId! It will (hopefully) be a while before this ID is used again + rmiResponseIds.offer(responseId) + } + + fun prep(rmiObjectId: Int, responseWaiter: SuspendWaiter): Int { + val responseId = rmiResponseIds.poll() + + // 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 } + + return responseId + } + + suspend fun waitForReply(allowWaiting: Boolean, isAsync: Boolean, rmiObjectId: Int, responseId: Int, + responseWaiter: SuspendWaiter, timeoutMillis: Long): Any? { + + val pendingId = RmiUtils.packShorts(rmiObjectId, responseId) + + var delayJobForTimeout: Job? = null + + 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" + + if (!isAsync) { + // we only cancel waiting because when NON-ASYNC + if (previous is SuspendWaiter) { + previous.cancel() + } + } + } + } + + return if (isAsync) { + null + } else { + waitForReplyManually(pendingId, responseWaiter, delayJobForTimeout) + } + } + + // this is called when we MANUALLY want to wait for a reply as part of async! + // 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() { + rmiResponseIds.clear() + pendingLock.write { pending.clear() } + } +} diff --git a/src/dorkbox/network/rmi/RmiServer.kt b/src/dorkbox/network/rmi/RmiServer.kt deleted file mode 100644 index 5fc5af9b..00000000 --- a/src/dorkbox/network/rmi/RmiServer.kt +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright 2010 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network.rmi - -import dorkbox.network.connection.Connection -import dorkbox.network.rmi.messages.MethodRequest -import dorkbox.network.rmi.messages.MethodResponse -import dorkbox.util.collections.LockFreeIntBiMap -import org.slf4j.Logger -import java.io.IOException -import java.util.* -import java.util.concurrent.atomic.AtomicInteger - -/** - * Allows methods on objects to be invoked remotely over TCP, UDP, or LOCAL. Local connections ignore TCP/UDP requests, and perform - * object transformation (because there is no serialization occurring) using a series of weak hashmaps. - * - * - * Objects are [NetworkSerializationManager.registerRmi], and endpoint connections can then [ ][Connection.createRemoteObject] for the registered objects. - * - * - * It costs at least 2 bytes more to use remote method invocation than just sending the parameters. If the method has a return value which - * is not [ignored][RemoteObject.setAsync], an extra byte is written. If the type of a parameter is not final (note that - * primitives are final) then an extra byte is written for that parameter. - * - * - * In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B. - * This is to support calling RMI methods from an interface (that does pass the connection reference) to - * an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use - * the interface, and the implType may override the method, so that we add the connection as the first in - * the list of parameters. - * - * - * for example: - * Interface: foo(String x) - * Impl: foo(Connection c, String x) - * - * - * The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface - * instead of the method that would NORMALLY be called. - * - * @author Nathan Sweet @n4te.com>, Nathan Robinson - */ -class RmiServer(// the name of who created this RmiBridge - val logger: Logger, isGlobal: Boolean) { - - companion object { - const val INVALID_MAP_ID = -1 - const val INVALID_RMI = Int.MAX_VALUE - - /** - * @return true if the objectId is global for all connections (even). false if it's connection-only (odd) - */ - fun isGlobal(objectId: Int): Boolean { - // global RMI objects -> EVEN in range 0 - (MAX_VALUE-1) - // connection local RMI -> ODD in range 1 - (MAX_VALUE-1) - return objectId and 1 == 0 - } - - /** - * Invokes the method on the object and, if necessary, sends the result back to the connection that made the invocation request. This - * method is invoked on the update thread of the [EndPoint] for this RmiBridge and unless an executor has been set. - * - * This is the response to the invoke method in the RmiProxyHandler - * - * @param connection - * The remote side of this connection requested the invocation. - */ - @Throws(IOException::class) - internal operator fun invoke(connection: Connection, target: Any, methodRequest: MethodRequest, logger: Logger): MethodResponse? { - val cachedMethod = methodRequest.cachedMethod - - if (logger.isTraceEnabled) { - var argString = "" - if (methodRequest.args != null) { - argString = Arrays.deepToString(methodRequest.args) - argString = argString.substring(1, argString.length - 1) - } - val stringBuilder = StringBuilder(128) - stringBuilder.append(connection.toString()) - .append(" received: ") - .append(target.javaClass.simpleName) - stringBuilder.append(":").append(methodRequest.objectId) - stringBuilder.append("#").append(cachedMethod.method.name) - stringBuilder.append("(").append(argString).append(")") - - if (cachedMethod.overriddenMethod != null) { - // did we override our cached method? THIS IS NOT COMMON. - stringBuilder.append(" [Connection method override]") - } - logger.trace(stringBuilder.toString()) - } - - - val responseId = methodRequest.responseId.toInt() - - var result: Any? - try { - // args!! is safe to do here (even though it doesn't make sense) - result = cachedMethod.invoke(connection, target, methodRequest.args!!) - } catch (ex: Exception) { - logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", ex) - - result = ex.cause - // added to prevent a stack overflow when references is false - // (because 'cause' == "this"). - // See: - // https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y - if (result == null) { - result = ex - } else { - result.initCause(null) - } - } - - // A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses - if (responseId == 0) { - return null - } - - val invokeMethodResult = MethodResponse() - invokeMethodResult.objectId = methodRequest.objectId - invokeMethodResult.responseId = responseId.toByte() - invokeMethodResult.result = result - - // logger.error("{} sent data: {} with id ({})", connection, result, invokeMethod.responseID); - return invokeMethodResult - } - } - - // we start at 1, because 0 (INVALID_RMI) means we access connection only objects - private val rmiObjectIdCounter: AtomicInteger = if (isGlobal) { - AtomicInteger(0) - } else { - AtomicInteger(1) - } - - // this is the ID -> Object RMI map. The RMI ID is used (not the kryo ID) - private val objectMap = LockFreeIntBiMap(INVALID_MAP_ID) - - - private fun nextObjectId(): Int { - // always increment by 2 - // global RMI objects -> ODD in range 1-16380 (max 2 bytes) throws error on outside of range - // connection local RMI -> EVEN in range 1-16380 (max 2 bytes) throws error on outside of range - val value = rmiObjectIdCounter.getAndAdd(2) - if (value >= INVALID_RMI) { - rmiObjectIdCounter.set(INVALID_RMI) // prevent wrapping by spammy callers - logger.error("next RMI value '{}' has exceeded maximum limit '{}' in RmiBridge! Not creating RMI object.", value, INVALID_RMI) - return INVALID_RMI - } - return value - } - - /** - * Automatically registers an object with the next available ID to allow the remote end of the RmiBridge connections to access it using the returned ID. - * - * @return the RMI object ID, if the registration failed (null object or TOO MANY objects), it will be [RmiServer.INVALID_RMI] (Integer.MAX_VALUE). - */ - fun register(`object`: Any?): Int { - if (`object` == null) { - return INVALID_RMI - } - - // this will return INVALID_RMI if there are too many in the ObjectSpace - val nextObjectId = nextObjectId() - if (nextObjectId != INVALID_RMI) { - // specifically avoid calling register(int, Object) method to skip non-necessary checks + exceptions - objectMap.put(nextObjectId, `object`) - if (logger.isTraceEnabled) { - logger.trace("Object registered with ObjectSpace with .toString() = '{}'", nextObjectId, `object`) - } - } - return nextObjectId - } - - /** - * Registers an object to allow the remote end of the RmiBridge connections to access it using the specified ID. - * - * @param objectID Must not be <0 or [RmiServer.INVALID_RMI] (Integer.MAX_VALUE). - */ - fun register(objectID: Int, `object`: Any?) { - require(objectID >= 0) { "objectID cannot be $INVALID_RMI" } - require(objectID < INVALID_RMI) { "objectID cannot be $INVALID_RMI" } - requireNotNull(`object`) { "object cannot be null." } - objectMap.put(objectID, `object`) - if (logger.isTraceEnabled) { - logger.trace("Object registered in RmiBridge with .toString() = '{}'", objectID, `object`) - } - } - - /** - * Removes an object. The remote end of the RmiBridge connection will no longer be able to access it. - */ - fun remove(objectID: Int) { - val `object` = objectMap.remove(objectID) - if (logger.isTraceEnabled) { - logger.trace("Object removed from ObjectSpace with .toString() = '{}'", objectID, `object`) - } - } - - /** - * Removes an object. The remote end of the RmiBridge connection will no longer be able to access it. - */ - fun remove(`object`: Any) { - val objectID = objectMap.inverse().remove(`object`) - if (objectID == INVALID_MAP_ID) { - logger.error("Object {} could not be found in the ObjectSpace.", `object`) - } else if (logger.isTraceEnabled) { - logger.trace("Object {} (ID: {}) removed from ObjectSpace.", `object`, objectID) - } - } - - /** - * Returns the object registered with the specified ID. - */ - fun getRegisteredObject(objectID: Int): Any? { - return if (objectID < 0 || objectID >= INVALID_RMI) { - null - } else objectMap[objectID] - - // Find an object with the objectID. - } - - /** - * Returns the ID registered for the specified object, or INVALID_RMI if not found. - */ - fun getRegisteredId(`object`: T): Int { - // Find an ID with the object. - val i = objectMap.inverse()[`object`] - return if (i == INVALID_MAP_ID) { - INVALID_RMI - } else i - } -} diff --git a/src/dorkbox/network/rmi/RmiSupport.kt b/src/dorkbox/network/rmi/RmiSupport.kt new file mode 100644 index 00000000..507a3f0a --- /dev/null +++ b/src/dorkbox/network/rmi/RmiSupport.kt @@ -0,0 +1,354 @@ +/* + * Copyright 2019 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.connection.Connection_ +import dorkbox.network.connection.EndPoint +import dorkbox.network.rmi.messages.* +import dorkbox.network.serialization.NetworkSerializationManager +import dorkbox.util.classes.ClassHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mu.KLogger +import java.lang.reflect.Proxy +import java.util.* + +class RmiSupport(logger: KLogger, + actionDispatch: CoroutineScope, + internal val serialization: NetworkSerializationManager) : RmiSupportCache(logger, actionDispatch) +{ + companion object { + /** + * Returns a proxy object that implements the specified interface, and the methods invoked on the proxy object will be invoked + * remotely. + * + * Methods that return a value will throw [TimeoutException] if the response is not received with the [RemoteObject.responseTimeout]. + * + * + * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving side will + * have the proxy object replaced with the registered object. + * + * @see RemoteObject + * + * @param rmiId this is the remote object ID (assigned by RMI). This is NOT the kryo registration ID + * @param interfaceClass this is the RMI interface class + */ + internal fun createProxyObject(isGlobalObject: Boolean, + connection: Connection, serialization: NetworkSerializationManager, + rmiSupportCache: RmiSupportCache, namePrefix: String, + rmiId: Int, interfaceClass: Class<*>): RemoteObject { + + require(interfaceClass.isInterface) { "iface must be an interface." } + + // duplicates are fine, as they represent the same object (as specified by the ID) on the remote side. + + val classId = serialization.getClassId(interfaceClass) + val cachedMethods = serialization.getMethods(classId) + + val name = "<${namePrefix}-proxy #$rmiId>" + + // the ACTUAL proxy is created in the connection impl. Our proxy handler MUST BE suspending because of: + // 1) how we send data on the wire + // 2) how we must (sometimes) wait for a response + val proxyObject = RmiClient(isGlobalObject, rmiId, connection, name, rmiSupportCache, cachedMethods) + + // This is the interface inheritance by the proxy object + val interfaces: Array> = arrayOf(RemoteObject::class.java, interfaceClass) + + return Proxy.newProxyInstance(RmiSupport::class.java.classLoader, interfaces, proxyObject) as RemoteObject + } + + /** + * Scans a class (+hierarchy) for @Rmi annotation and executes the 'registerAction' with it + */ + internal fun scanImplForRmiFields(logger: KLogger, implObject: Any, registerAction: (fieldObject: Any) -> Unit) { + val implementationClass = implObject::class.java + + // the @Rmi annotation allows an RMI object to have fields with objects that are ALSO RMI objects + val classesToCheck = LinkedList, Any?>>() + classesToCheck.add(AbstractMap.SimpleEntry(implementationClass, implObject)) + + + var remoteClassObject: Map.Entry, Any?> + while (!classesToCheck.isEmpty()) { + remoteClassObject = classesToCheck.removeFirst() + + // we have to check the IMPLEMENTATION for any additional fields that will have proxy information. + // we use getDeclaredFields() + walking the object hierarchy, so we get ALL the fields possible (public + private). + + for (field in remoteClassObject.key.declaredFields) { + if (field.getAnnotation(Rmi::class.java) != null) { + val type = field.type + if (!type.isInterface) { + // the type must be an interface, otherwise RMI cannot create a proxy object + logger.error("Error checking RMI fields for: {}.{} -- It is not an interface!", + remoteClassObject.key, + field.name) + continue + } +//TODO FIX THIS. MAYBE USE KOTLIN TO DO THIS? + val prev = field.isAccessible + field.isAccessible = true + + val o: Any + try { + o = field[remoteClassObject.value] + registerAction(o) + classesToCheck.add(AbstractMap.SimpleEntry(type, o)) + } catch (e: IllegalAccessException) { + logger.error("Error checking RMI fields for: {}.{}", remoteClassObject.key, field.name, e) + } finally { + field.isAccessible = prev + } + } + } + + // have to check the object hierarchy as well + val superclass = remoteClassObject.key.superclass + if (superclass != null && superclass != Any::class.java) { + classesToCheck.add(AbstractMap.SimpleEntry(superclass, remoteClassObject.value)) + } + } + } + + /** + * called on "client" + */ + private fun onGenericObjectResponse(endPoint: EndPoint, connection: Connection_, logger: KLogger, + isGlobal: Boolean, rmiId: Int, callback: suspend (Any) -> Unit, + rmiSupportCache: RmiSupportCache, serialization: NetworkSerializationManager) { + + // we only create the proxy + execute the callback if the RMI id is valid! + if (rmiId == RemoteObjectStorage.INVALID_RMI) { + logger.error { + "RMI ID '${rmiId}' is invalid. Unable to create RMI object on server." + } + return + } + + val interfaceClass = ClassHelper.getGenericParameterAsClassForSuperClass(RemoteObjectCallback::class.java, callback.javaClass, 0) + + // create the client-side proxy object, if possible + var proxyObject = rmiSupportCache.getProxyObject(rmiId) + if (proxyObject == null) { + proxyObject = createProxyObject(isGlobal, connection, serialization, rmiSupportCache, endPoint.type.simpleName, rmiId, interfaceClass) + rmiSupportCache.saveProxyObject(rmiId, proxyObject) + } + + // this should be executed on a NEW coroutine! + endPoint.actionDispatch.launch { + try { + callback(proxyObject) + } catch (e: Exception) { + logger.error("Error getting or creating the remote object $interfaceClass", e) + } + } + } + } + + // this is used for all connection specific ones as well. + private val remoteObjectCreationCallbacks = RemoteObjectStorage(logger) + + internal fun registerCallback(callback: suspend (Iface) -> Unit): Int { + return remoteObjectCreationCallbacks.register(callback) + } + + private fun removeCallback(callbackId: Int): suspend (Any) -> Unit { + return remoteObjectCreationCallbacks.remove(callbackId) + } + + /** + * Get's the implementation object based on if it is global, or not global + */ + fun getImplObject(isGlobal: Boolean, rmiObjectId: Int, connection: Connection_): Any { + return if (isGlobal) getImplObject(rmiObjectId) else connection.rmiSupport().getImplObject(rmiObjectId) + } + + override fun close() { + super.close() + remoteObjectCreationCallbacks.close() + } + + /** + * on the "client" to get a global remote object (that exists on the server) + */ + fun getGlobalRemoteObject(connection: C, endPoint: EndPoint, objectId: Int, interfaceClass: Class): Iface { + // 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) + var proxyObject = getProxyObject(objectId) + if (proxyObject == null) { + proxyObject = createProxyObject(true, connection, serialization, this, endPoint.type.simpleName, objectId, interfaceClass) + saveProxyObject(objectId, proxyObject) + } + + @Suppress("UNCHECKED_CAST") + return proxyObject as Iface + } + + /** + * on the "client" to create a global remote object (that exists on the server) + */ + suspend fun 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! + */ + @Throws(IllegalArgumentException::class) + suspend fun manage(endPoint: EndPoint, connection: Connection_, message: Any, logger: KLogger) { + when (message) { + is ConnectionObjectCreateRequest -> { + /** + * called on "server" + */ + connection.rmiSupport().onConnectionObjectCreateRequest(endPoint, connection, message, logger) + } + is ConnectionObjectCreateResponse -> { + /** + * called on "client" + */ + val rmiId = RmiUtils.unpackLeft(message.packedIds) + val callbackId = RmiUtils.unpackRight(message.packedIds) + val callback = removeCallback(callbackId) + onGenericObjectResponse(endPoint, connection, logger, false, rmiId, callback, this, serialization) + } + is GlobalObjectCreateRequest -> { + /** + * called on "server" + */ + onGlobalObjectCreateRequest(endPoint, connection, message, logger) + } + is GlobalObjectCreateResponse -> { + /** + * called on "client" + */ + val rmiId = RmiUtils.unpackLeft(message.packedIds) + val callbackId = RmiUtils.unpackRight(message.packedIds) + val callback = removeCallback(callbackId) + onGenericObjectResponse(endPoint, connection, logger, true, rmiId, callback, this, serialization) + } + is MethodRequest -> { + /** + * Invokes the method on the object and, sends the result back to the connection that made the invocation request. + * + * This is the response to the invoke method in the RmiClient + * + * The remote side of this connection requested the invocation. + */ + val objectId: Int = message.objectId + val isGlobal: Boolean = message.isGlobal + val cachedMethod = message.cachedMethod + + val implObject = getImplObject(isGlobal, objectId, connection) + + logger.trace { + var argString = "" + if (message.args != null) { + argString = Arrays.deepToString(message.args) + argString = argString.substring(1, argString.length - 1) + } + + val stringBuilder = StringBuilder(128) + stringBuilder.append(connection.toString()) + .append(" received: ") + .append(implObject.javaClass.simpleName) + stringBuilder.append(":").append(objectId) + stringBuilder.append("#").append(cachedMethod.method.name) + stringBuilder.append("(").append(argString).append(")") + + if (cachedMethod.overriddenMethod != null) { + // did we override our cached method? THIS IS NOT COMMON. + stringBuilder.append(" [Connection method override]") + } + stringBuilder.toString() + } + + + var result: Any? + try { + // args!! is safe to do here (even though it doesn't make sense) + result = cachedMethod.invoke(connection, implObject, message.args!!) + } catch (ex: Exception) { + logger.error("Error invoking method: ${cachedMethod.method.declaringClass.name}.${cachedMethod.method.name}", ex) + + result = ex.cause + // added to prevent a stack overflow when references is false, (because 'cause' == "this"). + // See: + // https://groups.google.com/forum/?fromgroups=#!topic/kryo-users/6PDs71M1e9Y + if (result == null) { + result = ex + } else { + result.initCause(null) + } + } + + val invokeMethodResult = MethodResponse() + invokeMethodResult.objectId = objectId + invokeMethodResult.responseId = message.responseId + invokeMethodResult.result = result + + connection.send(invokeMethodResult) + } + is MethodResponse -> { + // notify the pending proxy requests that we have a response! + getResponseStorage().onMessage(message) + } + } + } + + /** + * called on "server" + */ + private suspend fun onGlobalObjectCreateRequest( + endPoint: EndPoint, connection: Connection_, message: GlobalObjectCreateRequest, logger: KLogger) { + + val interfaceClassId = RmiUtils.unpackLeft(message.packedIds) + val callbackId = RmiUtils.unpackRight(message.packedIds) + + // We have to lookup the iface, since the proxy object requires it + val implObject = endPoint.serialization.createRmiObject(interfaceClassId) + val rmiId = registerImplObject(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! + connection.send(GlobalObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId))) + } +} diff --git a/src/dorkbox/network/rmi/RmiSupportCache.kt b/src/dorkbox/network/rmi/RmiSupportCache.kt new file mode 100644 index 00000000..0c820f83 --- /dev/null +++ b/src/dorkbox/network/rmi/RmiSupportCache.kt @@ -0,0 +1,55 @@ +package dorkbox.network.rmi + +import dorkbox.util.collections.LockFreeIntMap +import kotlinx.coroutines.CoroutineScope +import mu.KLogger + +/** + * Cache for implementation and proxy objects. + * + * 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) + */ +open class RmiSupportCache(logger: KLogger, actionDispatch: CoroutineScope) { + + private val responseStorage = RmiResponseStorage(actionDispatch) + private val implObjects = RemoteObjectStorage(logger) + private val proxyObjects = LockFreeIntMap() + + fun registerImplObject(rmiObject: Any): Int { + return implObjects.register(rmiObject) + } + + fun getImplObject(rmiId: Int): Any { + return implObjects[rmiId] + } + + fun removeImplObject(rmiId: Int) { + implObjects.remove(rmiId) as Any + } + + /** + * Removes a proxy object from the system + */ + fun removeProxyObject(rmiId: Int) { + proxyObjects.remove(rmiId) + } + + fun getProxyObject(rmiId: Int): RemoteObject? { + return proxyObjects[rmiId] + } + + fun saveProxyObject(rmiId: Int, remoteObject: RemoteObject) { + proxyObjects.put(rmiId, remoteObject) + } + + fun getResponseStorage(): RmiResponseStorage { + return responseStorage + } + + open fun close() { + implObjects.close() + proxyObjects.clear() + responseStorage.close() + } +} diff --git a/src/dorkbox/network/rmi/RmiSupportConnection.kt b/src/dorkbox/network/rmi/RmiSupportConnection.kt new file mode 100644 index 00000000..3ed134bb --- /dev/null +++ b/src/dorkbox/network/rmi/RmiSupportConnection.kt @@ -0,0 +1,80 @@ +package dorkbox.network.rmi + +import dorkbox.network.connection.Connection +import dorkbox.network.connection.Connection_ +import dorkbox.network.connection.EndPoint +import dorkbox.network.rmi.messages.ConnectionObjectCreateRequest +import dorkbox.network.rmi.messages.ConnectionObjectCreateResponse +import dorkbox.network.serialization.NetworkSerializationManager +import kotlinx.coroutines.CoroutineScope +import mu.KLogger + +class RmiSupportConnection(logger: KLogger, + private val rmiGlobalSupport: RmiSupport, + private val serialization: NetworkSerializationManager, + actionDispatch: CoroutineScope) : RmiSupportCache(logger, actionDispatch) { + + /** + * on the "client" to get a connection-specific remote object (that exists on the server) + */ + fun getRemoteObject(connection: Connection, endPoint: EndPoint, objectId: Int, interfaceClass: Class): Iface { + // 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) + var proxyObject = getProxyObject(objectId) + if (proxyObject == null) { + proxyObject = RmiSupport.createProxyObject(false, connection, serialization, rmiGlobalSupport, endPoint.type.simpleName, objectId, interfaceClass) + saveProxyObject(objectId, proxyObject) + } + + @Suppress("UNCHECKED_CAST") + return proxyObject as Iface + } + + + /** + * on the "client" to create a connection-specific remote object (that exists on the server) + */ + suspend fun createRemoteObject(connection: C, interfaceClassId: Int, callback: suspend (Iface) -> Unit) { + val callbackId = rmiGlobalSupport.registerCallback(callback) + + // There is no rmiID yet, because we haven't created it! + val message = ConnectionObjectCreateRequest(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) + } + + /** + * called on "server" + */ + internal suspend fun onConnectionObjectCreateRequest( + endPoint: EndPoint, connection: Connection_, message: ConnectionObjectCreateRequest, logger: KLogger) { + + val interfaceClassId = RmiUtils.unpackLeft(message.packedIds) + val callbackId = RmiUtils.unpackRight(message.packedIds) + + // We have to lookup the iface, since the proxy object requires it + val implObject = endPoint.serialization.createRmiObject(interfaceClassId) + val rmiId = registerImplObject(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 + RmiSupport.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! + connection.send(ConnectionObjectCreateResponse(RmiUtils.packShorts(rmiId, callbackId))) + } +} diff --git a/src/dorkbox/network/rmi/RmiUtils.kt b/src/dorkbox/network/rmi/RmiUtils.kt index 9ac02973..a52d21e3 100644 --- a/src/dorkbox/network/rmi/RmiUtils.kt +++ b/src/dorkbox/network/rmi/RmiUtils.kt @@ -402,4 +402,17 @@ object RmiUtils { allClasses.remove(clazz) return allClasses } + + private const val RIGHT = 0xFFFF + fun packShorts(left: Int, right: Int): Int { + return left shl 16 or (right and RIGHT) + } + + fun unpackLeft(packedInt: Int): Int { + return packedInt ushr 16 // >>> operator 0-fills from left + + } + fun unpackRight(packedInt: Int): Int { + return packedInt and RIGHT + } } diff --git a/src/dorkbox/network/rmi/messages/ConnectionObjectCreateRequest.kt b/src/dorkbox/network/rmi/messages/ConnectionObjectCreateRequest.kt new file mode 100644 index 00000000..c091d7a9 --- /dev/null +++ b/src/dorkbox/network/rmi/messages/ConnectionObjectCreateRequest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010 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.messages + +/** + * These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints. + * + * @param interfaceClassId (LEFT) the Kryo interface class ID to create + * @param callbackId (RIGHT) to know which callback to use when the object is created + */ +data class ConnectionObjectCreateRequest(val packedIds: Int) : RmiMessage diff --git a/test-orig/dorkbox/network/rmi/MessageWithTestCow.java b/src/dorkbox/network/rmi/messages/ConnectionObjectCreateResponse.kt similarity index 59% rename from test-orig/dorkbox/network/rmi/MessageWithTestCow.java rename to src/dorkbox/network/rmi/messages/ConnectionObjectCreateResponse.kt index 5f028e09..0f5f237c 100644 --- a/test-orig/dorkbox/network/rmi/MessageWithTestCow.java +++ b/src/dorkbox/network/rmi/messages/ConnectionObjectCreateResponse.kt @@ -1,9 +1,10 @@ /* - * Copyright 2019 dorkbox, llc. + * Copyright 2010 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 @@ -12,29 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dorkbox.network.rmi; +package dorkbox.network.rmi.messages + /** + * These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints. * + * @param rmiId (LEFT) the Kryo interface class ID to create + * @param callbackId (RIGHT) to know which callback to use when the object is created */ -public -class MessageWithTestCow { - public int number; - public String text; - private TestCow testCow; - - private - MessageWithTestCow() { - // for kryo - } - - public - MessageWithTestCow(final TestCow test) { - testCow = test; - } - - public - TestCow getTestCow() { - return testCow; - } -} +data class ConnectionObjectCreateResponse(val packedIds: Int) : RmiMessage diff --git a/src/dorkbox/network/rmi/messages/DORequestSerializer.kt b/src/dorkbox/network/rmi/messages/DORequestSerializer.kt deleted file mode 100644 index 68a4361c..00000000 --- a/src/dorkbox/network/rmi/messages/DORequestSerializer.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2010 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.messages - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import dorkbox.network.connection.KryoExtra -import dorkbox.network.rmi.RmiServer - -/** - * This is required, because with RMI, it is possible that the IMPL and IFACE can have DIFFERENT class IDs, in which case, the "client" cannot read the correct - * objects (because the IMPL class might not be registered, or that ID might be registered to a different class) - */ -class DORequestSerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, objectRequest: DynamicObjectRequest) { - output.writeInt(objectRequest.rmiId, true) - output.writeInt(objectRequest.callbackId, true) - - var id = kryo.getRegistration(objectRequest.interfaceClass).id - output.writeInt(id, true) - - id = if (objectRequest.remoteObject != null) { - val kryoExtra = kryo as KryoExtra - kryoExtra.rmiSupport.getRegisteredId(objectRequest.remoteObject) - } else { - // can be < 0 or >= RmiBridge.INVALID_RMI (Integer.MAX_VALUE) - -1 - } - - output.writeInt(id, false) - } - - override fun read(kryo: Kryo, input: Input, implementationType: Class): DynamicObjectRequest { - val rmiId = input.readInt(true) - val callbackId = input.readInt(true) - val interfaceClassId = input.readInt(true) - val remoteObjectId = input.readInt(false) - - - // // We have to lookup the iface, since the proxy object requires it - val iface = kryo.getRegistration(interfaceClassId).type - var remoteObject: Any? = null - - if (remoteObjectId >= 0 && remoteObjectId < RmiServer.INVALID_RMI) { - val kryoExtra = kryo as KryoExtra - val connection = kryoExtra.connection - remoteObject = kryoExtra.rmiSupport.getProxyObject(connection, remoteObjectId, iface) - } - - return DynamicObjectRequest(iface, rmiId, callbackId, remoteObject) - } -} diff --git a/src/dorkbox/network/rmi/messages/DOResponseSerializer.kt b/src/dorkbox/network/rmi/messages/DOResponseSerializer.kt deleted file mode 100644 index 489a00d7..00000000 --- a/src/dorkbox/network/rmi/messages/DOResponseSerializer.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2010 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.messages - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import dorkbox.network.connection.KryoExtra -import dorkbox.network.rmi.RmiServer - -/** - * This is required, because with RMI, it is possible that the IMPL and IFACE can have DIFFERENT class IDs, in which case, the "client" cannot read the correct - * objects (because the IMPL class might not be registered, or that ID might be registered to a different class) - */ -class DOResponseSerializer : Serializer() { - override fun write(kryo: Kryo, output: Output, objectResponse: DynamicObjectResponse) { - output.writeInt(objectResponse.rmiId, true) - output.writeInt(objectResponse.callbackId, true) - - var id = kryo.getRegistration(objectResponse.interfaceClass).id - output.writeInt(id, true) - - id = if (objectResponse.remoteObject != null) { - val kryoExtra = kryo as KryoExtra - kryoExtra.rmiSupport.getRegisteredId(objectResponse.remoteObject) - } else { - // can be < 0 or >= RmiBridge.INVALID_RMI (Integer.MAX_VALUE) - -1 - } - - output.writeInt(id, false) - } - - override fun read(kryo: Kryo, input: Input, implementationType: Class): DynamicObjectResponse { - val rmiId = input.readInt(true) - val callbackId = input.readInt(true) - val interfaceClassId = input.readInt(true) - val remoteObjectId = input.readInt(false) - - - // // We have to lookup the iface, since the proxy object requires it - val iface = kryo.getRegistration(interfaceClassId).type - var remoteObject: Any? = null - - if (remoteObjectId >= 0 && remoteObjectId < RmiServer.INVALID_RMI) { - val kryoExtra = kryo as KryoExtra - val connection = kryoExtra.connection - remoteObject = kryoExtra.rmiSupport.getProxyObject(connection, remoteObjectId, iface) - } - - return DynamicObjectResponse(iface, rmiId, callbackId, remoteObject) - } -} diff --git a/src/dorkbox/network/rmi/messages/DynamicObjectRequest.kt b/src/dorkbox/network/rmi/messages/DynamicObjectRequest.kt deleted file mode 100644 index 0be1aac8..00000000 --- a/src/dorkbox/network/rmi/messages/DynamicObjectRequest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2010 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.messages - -/** - * Message specifically to register a class implementation for RMI - */ -class DynamicObjectRequest : RmiMessage { - /** - * this is null if there are problems creating an object on the remote side, otherwise it is non-null. - */ - var remoteObject: Any? = null - - /** - * this is used to create a NEW rmi object on the REMOTE side (these are bound the to connection. They are NOT GLOBAL, ie: available on all connections) - */ - var interfaceClass: Class<*> - - /** - * this is used to get specific, GLOBAL rmi objects (objects that are not bound to a single connection) - */ - var rmiId: Int - - /** - * this is the callback ID assigned by the LOCAL side, to know WHICH RMI callback to call when we have a remote object available - */ - var callbackId: Int - - /** - * When requesting a new or existing remote object - * SENT FROM "local" -> "remote" - * - * @param interfaceClass the class to create - * @param rmiId the RMI id to get from the REMOTE side - * @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use - */ - constructor(interfaceClass: Class<*>, rmiId: Int, callbackId: Int) { - this.interfaceClass = interfaceClass - this.rmiId = rmiId - this.callbackId = callbackId - } - - /** - * This is when we successfully created a new object (if there was an error, remoteObject is null) - * SENT FROM "remote" -> "local" - * - * @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use - */ - constructor(interfaceClass: Class<*>, rmiId: Int, callbackId: Int, remoteObject: Any?) { -// this.isRequest = false - this.interfaceClass = interfaceClass - this.rmiId = rmiId - this.callbackId = callbackId - this.remoteObject = remoteObject - } -} diff --git a/src/dorkbox/network/rmi/messages/DynamicObjectResponse.kt b/src/dorkbox/network/rmi/messages/DynamicObjectResponse.kt deleted file mode 100644 index 13d7b90c..00000000 --- a/src/dorkbox/network/rmi/messages/DynamicObjectResponse.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2010 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.messages - -/** - * This is when we successfully created a new object (if there was an error, remoteObject is null) - * SENT FROM "remote" -> "local" - * - * @param callbackId the rmi callback ID on the LOCAL side, to know which callback to use - */ -class DynamicObjectResponse(interfaceClass: Class<*>, rmiId: Int, callbackId: Int, remoteObject: Any?) : RmiMessage { - /** - * this is null if there are problems creating an object on the remote side, otherwise it is non-null. - */ - var remoteObject: Any? = null - - /** - * this is used to create a NEW rmi object on the REMOTE side (these are bound the to connection. They are NOT GLOBAL, ie: available on all connections) - */ - var interfaceClass: Class<*> - - /** - * this is used to get specific, GLOBAL rmi objects (objects that are not bound to a single connection) - */ - var rmiId: Int - - /** - * this is the callback ID assigned by the LOCAL side, to know WHICH RMI callback to call when we have a remote object available - */ - var callbackId: Int - - - init { - this.interfaceClass = interfaceClass - this.rmiId = rmiId - this.callbackId = callbackId - this.remoteObject = remoteObject - } -} diff --git a/src/dorkbox/network/rmi/messages/GlobalObjectCreateRequest.kt b/src/dorkbox/network/rmi/messages/GlobalObjectCreateRequest.kt new file mode 100644 index 00000000..b01b6a50 --- /dev/null +++ b/src/dorkbox/network/rmi/messages/GlobalObjectCreateRequest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010 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.messages + +/** + * These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints. + * + * @param interfaceClassId (LEFT) the Kryo interface class ID to create + * @param callbackId (RIGHT) to know which callback to use when the object is created + */ +data class GlobalObjectCreateRequest(val packedIds: Int) : RmiMessage diff --git a/src/dorkbox/network/rmi/messages/GlobalObjectCreateResponse.kt b/src/dorkbox/network/rmi/messages/GlobalObjectCreateResponse.kt new file mode 100644 index 00000000..0dd2fda1 --- /dev/null +++ b/src/dorkbox/network/rmi/messages/GlobalObjectCreateResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2010 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.messages + + +/** + * These use packed IDs, because both are REALLY shorts, but the JVM deals better with ints. + * + * @param rmiId (LEFT) the Kryo interface class ID to create + * @param callbackId (RIGHT) to know which callback to use when the object is created + */ +data class GlobalObjectCreateResponse(val packedIds: Int) : RmiMessage diff --git a/src/dorkbox/network/rmi/messages/MethodRequest.kt b/src/dorkbox/network/rmi/messages/MethodRequest.kt index b5a63176..926cb0a4 100644 --- a/src/dorkbox/network/rmi/messages/MethodRequest.kt +++ b/src/dorkbox/network/rmi/messages/MethodRequest.kt @@ -39,16 +39,23 @@ import dorkbox.network.rmi.CachedMethod /** * Internal message to invoke methods remotely. */ -class MethodRequest internal constructor() : RmiMessage { - // the registered kryo ID for the object - var objectId = 0 +class MethodRequest : RmiMessage { + // if this object was a global or connection specific object + var isGlobal: Boolean = false - // This class is NOT sent across the wire (but it's contents are!). We use a custom serializer to manage this. + // the registered kryo ID for the object + // NOTE: this is REALLY a short, but is represented as an int to make life easier. It is also packed with the responseId for serialization + var objectId: Int = 0 + + // A value of 0 means to not respond, otherwise it is an ID to match requests <-> responses + // NOTE: this is REALLY a short, but is represented as an int to make life easier. It is also packed with the objectId for serialization + var responseId: Int = 0 + + // This field is NOT sent across the wire (but some of it's contents are). + // We use a custom serializer to manage this because we have to ALSO be able to serialize the invocation arguments. + // NOTE: the info we serialze is REALLY a short, but is represented as an int to make life easier. It is also packed! lateinit var cachedMethod: CachedMethod - // these are the arguments for executing the method + // these are the arguments for executing the method (they are serialized using the info from the cachedMethod field var args: Array? = null - - // A value of 0 means to not respond, and the rest is just an ID to match requests <-> responses - var responseId: Byte = 0 } diff --git a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt index 4c2fc460..16cebf0b 100644 --- a/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt +++ b/src/dorkbox/network/rmi/messages/MethodRequestSerializer.kt @@ -40,60 +40,78 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import dorkbox.network.connection.KryoExtra +import dorkbox.network.rmi.RmiUtils import java.lang.reflect.Method /** * Internal message to invoke methods remotely. */ +@Suppress("ConstantConditionIf") class MethodRequestSerializer : Serializer() { companion object { private const val DEBUG = false } override fun write(kryo: Kryo, output: Output, methodRequest: MethodRequest) { + val method = methodRequest.cachedMethod + if (DEBUG) { System.err.println("WRITING") - System.err.println(":: objectID " + methodRequest.objectId) - System.err.println(":: methodClassID " + methodRequest.cachedMethod.methodClassId) - System.err.println(":: methodIndex " + methodRequest.cachedMethod.methodIndex) + System.err.println(":: isGlobal ${methodRequest.isGlobal}") + System.err.println(":: objectID ${methodRequest.objectId}") + System.err.println(":: methodClassID ${method.methodClassId}") + System.err.println(":: methodIndex ${method.methodIndex}") } - output.writeInt(methodRequest.objectId, true) - output.writeInt(methodRequest.cachedMethod.methodClassId, true) - output.writeByte(methodRequest.cachedMethod.methodIndex) + // we pack objectId + responseId into the same "int", since they are both really shorts (but are represented as ints to make + // working with them a lot easier + output.writeInt(RmiUtils.packShorts(methodRequest.objectId, methodRequest.responseId), true) + output.writeInt(RmiUtils.packShorts(method.methodClassId, method.methodIndex), true) + output.writeBoolean(methodRequest.isGlobal) - val serializers = methodRequest.cachedMethod.serializers - val length = serializers.size - val args = methodRequest.args - for (i in 0 until length) { - val serializer = serializers[i] - if (serializer != null) { - kryo.writeObjectOrNull(output, args!![i], serializer) - } else { - kryo.writeClassAndObject(output, args!![i]) + + val serializers = method.serializers + if (serializers.isNotEmpty()) { + val args = methodRequest.args!! + + serializers.forEachIndexed { index, serializer -> + if (serializer != null) { + kryo.writeObjectOrNull(output, args[index], serializer) + } else { + kryo.writeClassAndObject(output, args[index]) + } } } - - output.writeByte(methodRequest.responseId) } + @Suppress("UNCHECKED_CAST") override fun read(kryo: Kryo, input: Input, type: Class): MethodRequest { - val objectID = input.readInt(true) - val methodClassID = input.readInt(true) - val methodIndex = input.readByte() + val objectIdRmiId = input.readInt(true) + val objectId = RmiUtils.unpackLeft(objectIdRmiId) + val responseId = RmiUtils.unpackRight(objectIdRmiId) + + + val methodInfo = input.readInt(true) + val methodClassId = RmiUtils.unpackLeft(methodInfo) + val methodIndex = RmiUtils.unpackRight(methodInfo) + + val isGlobal = input.readBoolean() if (DEBUG) { System.err.println("READING") - System.err.println(":: objectID $objectID") - System.err.println(":: methodClassID $methodClassID") + System.err.println(":: isGlobal $isGlobal") + System.err.println(":: objectID $objectId") + System.err.println(":: methodClassID $methodClassId") System.err.println(":: methodIndex $methodIndex") } + (kryo as KryoExtra) + val cachedMethod = try { - (kryo as KryoExtra).serializationManager.getMethods(methodClassID)[methodIndex.toInt()] + kryo.getMethods(methodClassId)[methodIndex] } catch (ex: Exception) { - val methodClass = kryo.getRegistration(methodClassID).type + val methodClass = kryo.getRegistration(methodClassId).type throw KryoException("Invalid method index " + methodIndex + " for class: " + methodClass.name) } @@ -109,6 +127,8 @@ class MethodRequestSerializer : Serializer() { // this is specifically when we override an interface method, with an implementation method + Connection parameter (@ index 0) argStartIndex = 1 args = arrayOfNulls(serializers.size + 1) as Array + + // we have to save the connection this happened on, so it can be part of the method invocation args[0] = kryo.connection } else { method = cachedMethod.method @@ -118,28 +138,29 @@ class MethodRequestSerializer : Serializer() { val parameterTypes = method.parameterTypes - // we don't start at 0 for the arguments, in case we have an overwritten method (in which case, the 1st arg is always "Connection.class") - var i = 0 - val n = serializers.size - var j = argStartIndex + // we don't start at 0 for the arguments, in case we have an overwritten method, in which case, the 1st arg is always "Connection.class" + var index = 0 + val size = serializers.size + var argStart = argStartIndex - while (i < n) { - val serializer = serializers[i] + while (index < size) { + val serializer = serializers[index] if (serializer != null) { - args[j] = kryo.readObjectOrNull(input, parameterTypes[i], serializer) + args[argStart] = kryo.readObjectOrNull(input, parameterTypes[index], serializer) } else { - args[j] = kryo.readClassAndObject(input) + args[argStart] = kryo.readClassAndObject(input) } - i++ - j++ + index++ + argStart++ } val invokeMethod = MethodRequest() - invokeMethod.objectId = objectID + invokeMethod.isGlobal = isGlobal + invokeMethod.objectId = objectId invokeMethod.cachedMethod = cachedMethod invokeMethod.args = args - invokeMethod.responseId = input.readByte() + invokeMethod.responseId = responseId return invokeMethod } diff --git a/src/dorkbox/network/rmi/messages/MethodResponse.kt b/src/dorkbox/network/rmi/messages/MethodResponse.kt index 02701ccd..653fe0a9 100644 --- a/src/dorkbox/network/rmi/messages/MethodResponse.kt +++ b/src/dorkbox/network/rmi/messages/MethodResponse.kt @@ -38,12 +38,14 @@ package dorkbox.network.rmi.messages * Internal message to return the result of a remotely invoked method. */ class MethodResponse : RmiMessage { - // the registered kryo ID for the object - var objectId = 0 + // if this object was a global or connection specific object + var isGlobal: Boolean = false - // A value of 0 means to not respond (this object is NOT created if the request 'responseId = 0' - // This is just an ID to match requests <-> responses - var responseId: Byte = 0 + // the registered kryo ID for the object + var objectId: Int = 0 + + // ID to match requests <-> responses + var responseId: Int = 0 // this is the result of the invoked method var result: Any? = null diff --git a/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt b/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt index ae2713c7..786abd1b 100644 --- a/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt +++ b/src/dorkbox/network/rmi/messages/MethodResponseSerializer.kt @@ -19,18 +19,22 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output +import dorkbox.network.rmi.RmiUtils class MethodResponseSerializer() : Serializer() { - override fun write(kryo: Kryo, output: Output, methodResponse: MethodResponse) { - output.writeInt(methodResponse.objectId, true) - output.writeByte(methodResponse.responseId) - kryo.writeClassAndObject(output, methodResponse.result) + override fun write(kryo: Kryo, output: Output, response: MethodResponse) { + output.writeInt(RmiUtils.packShorts(response.objectId, response.responseId), true) + output.writeBoolean(response.isGlobal) + kryo.writeClassAndObject(output, response.result) } override fun read(kryo: Kryo, input: Input, type: Class): MethodResponse { + val packedInfo = input.readInt(true) + val response = MethodResponse() - response.objectId = input.readInt(true) - response.responseId = input.readByte() + response.objectId = RmiUtils.unpackLeft(packedInfo) + response.responseId = RmiUtils.unpackRight(packedInfo) + response.isGlobal = input.readBoolean() response.result = kryo.readClassAndObject(input) return response diff --git a/src/dorkbox/network/rmi/messages/ObjectResponseSerializer.kt b/src/dorkbox/network/rmi/messages/ObjectResponseSerializer.kt index c4f25ab6..642221ab 100644 --- a/src/dorkbox/network/rmi/messages/ObjectResponseSerializer.kt +++ b/src/dorkbox/network/rmi/messages/ObjectResponseSerializer.kt @@ -50,17 +50,19 @@ import dorkbox.network.connection.KryoExtra class ObjectResponseSerializer(private val rmiImplToIface: IdentityMap, Class<*>>) : Serializer(false) { override fun write(kryo: Kryo, output: Output, `object`: Any) { val kryoExtra = kryo as KryoExtra - val id = kryoExtra.rmiSupport.getRegisteredId(`object`) // - output.writeInt(id, true) +// val id = kryoExtra.rmiSupport.getRegisteredId(`object`) // +// output.writeInt(id, true) + output.writeInt(0, true) } - override fun read(kryo: Kryo, input: Input, implementationType: Class<*>): Any { + override fun read(kryo: Kryo, input: Input, implementationType: Class<*>): Any? { val kryoExtra = kryo as KryoExtra val objectID = input.readInt(true) // We have to lookup the iface, since the proxy object requires it val iface = rmiImplToIface.get(implementationType) val connection = kryoExtra.connection - return kryoExtra.rmiSupport.getProxyObject(connection, objectID, iface) +// return kryoExtra.rmiSupport.getProxyObject(connection, objectID, iface) + return null } } diff --git a/src/dorkbox/network/rmi/messages/ObjectRequestSerializer.kt b/src/dorkbox/network/rmi/messages/RmiClientRequestSerializer.kt similarity index 74% rename from src/dorkbox/network/rmi/messages/ObjectRequestSerializer.kt rename to src/dorkbox/network/rmi/messages/RmiClientRequestSerializer.kt index b888e435..c8066eb4 100644 --- a/src/dorkbox/network/rmi/messages/ObjectRequestSerializer.kt +++ b/src/dorkbox/network/rmi/messages/RmiClientRequestSerializer.kt @@ -21,27 +21,25 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import dorkbox.network.connection.KryoExtra import dorkbox.network.rmi.RmiClient -import org.slf4j.Logger import java.lang.reflect.Proxy /** - * this is to manage serializing proxy object objects across the wire + * this is to manage serializing proxy object objects across the wire... + * SO the server sends an RMI object, and the client reads an RMI object */ -class ObjectRequestSerializer(private val logger: Logger) : Serializer() { +class RmiClientRequestSerializer : Serializer() { override fun write(kryo: Kryo, output: Output, proxyObject: Any) { val handler = Proxy.getInvocationHandler(proxyObject) as RmiClient + output.writeBoolean(handler.isGlobal) output.writeInt(handler.rmiObjectId, true) } override fun read(kryo: Kryo, input: Input, type: Class<*>?): Any? { - val objectID = input.readInt(true) - val kryoExtra = kryo as KryoExtra + val isGlobal = input.readBoolean() + val objectId = input.readInt(true) + kryo as KryoExtra - val `object` = kryoExtra.rmiSupport.getImplementationObject(objectID) - if (`object` == null) { - logger.error("Unknown object ID in RMI ObjectSpace: {}", objectID) - } - - return `object` + val connection = kryo.connection + return connection.endPoint().rmiSupport.getImplObject(isGlobal, objectId, connection) } } diff --git a/src/dorkbox/network/serialization/NetworkSerializationManager.kt b/src/dorkbox/network/serialization/NetworkSerializationManager.kt index ab5f37fa..c7e6d882 100644 --- a/src/dorkbox/network/serialization/NetworkSerializationManager.kt +++ b/src/dorkbox/network/serialization/NetworkSerializationManager.kt @@ -141,13 +141,30 @@ interface NetworkSerializationManager : SerializationManager { /** * @return true if the remote kryo registration are the same as our own */ - fun verifyKryoRegistration(bytes: ByteArray): Boolean + fun verifyKryoRegistration(clientBytes: ByteArray): Boolean /** * @return the details of all registration IDs -> Class name used by kryo */ fun getKryoRegistrationDetails(): ByteArray + /** + * Creates a NEW object implementation based on the KRYO interface ID. + * + * @return the corresponding implementation object + */ + fun createRmiObject(interfaceClassId: Int): Any + + /** + * Returns the Kryo class registration ID + */ + fun getClassId(iFace: Class<*>): Int + + /** + * Returns the Kryo class from a registration ID + */ + fun getClassFromId(interfaceClassId: Int): Class<*> + /** * Gets the RMI implementation based on the specified interface * diff --git a/src/dorkbox/network/serialization/Serialization.kt b/src/dorkbox/network/serialization/Serialization.kt index 152954f4..00f7e9b3 100644 --- a/src/dorkbox/network/serialization/Serialization.kt +++ b/src/dorkbox/network/serialization/Serialization.kt @@ -15,20 +15,15 @@ */ package dorkbox.network.serialization -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Registration -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.SerializerFactory +import com.esotericsoftware.kryo.* import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy import com.esotericsoftware.kryo.util.IdentityMap import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer -import dorkbox.network.connection.Connection_ import dorkbox.network.connection.KryoExtra import dorkbox.network.connection.ping.PingMessage import dorkbox.network.rmi.CachedMethod -import dorkbox.network.rmi.NopRmiConnection import dorkbox.network.rmi.RmiUtils import dorkbox.network.rmi.messages.* import dorkbox.objectPool.ObjectPool @@ -37,6 +32,7 @@ import dorkbox.util.OS import io.netty.buffer.ByteBuf import io.netty.buffer.Unpooled import org.agrona.collections.Int2ObjectHashMap +import org.objenesis.instantiator.ObjectInstantiator import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -70,6 +66,26 @@ class Serialization(references: Boolean, companion object { const val CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE = 400 + private val UNMODIFIABLE_COLLECTION_SERIALIZERS: Array, Serializer>> + + init { + val unmodSerializers = mutableListOf, Serializer>>() + + // hacky way to register unmodifiable serializers. This MUST be done here, because we ONLY want internal objects created once + @Suppress("UNCHECKED_CAST") + val kryo: Kryo = object : Kryo() { + override fun register(type: Class<*>, serializer: Serializer<*>): Registration { + val type1 = type as Class + val serializer1 = serializer as Serializer + unmodSerializers.add(Pair(type1, serializer1)) + return super.register(type, serializer) + } + } + UnmodifiableCollectionsSerializer.registerSerializers(kryo) + + UNMODIFIABLE_COLLECTION_SERIALIZERS = unmodSerializers.toTypedArray() + // end hack + } /** * Additionally, this serialization manager will register the entire class+interface hierarchy for an object. If you want to specify a @@ -86,41 +102,9 @@ class Serialization(references: Boolean, fun DEFAULT(references: Boolean = true, factory: SerializerFactory<*>? = null): Serialization { val serialization = Serialization(references, factory) - // these are registered using the default serializers - serialization.register(String::class.java) - serialization.register(Array::class.java) - - serialization.register(IntArray::class.java) - serialization.register(ShortArray::class.java) - serialization.register(FloatArray::class.java) - serialization.register(DoubleArray::class.java) - serialization.register(LongArray::class.java) - serialization.register(ByteArray::class.java) - serialization.register(CharArray::class.java) - serialization.register(BooleanArray::class.java) - - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - serialization.register(Array::class.java) - - - serialization.register(Array::class.java) - serialization.register(Array>::class.java) - serialization.register(Class::class.java) - - // necessary for the transport of exceptions. - serialization.register(StackTraceElement::class.java) - serialization.register(Array::class.java) - serialization.register(ControlMessage::class.java) serialization.register(PingMessage::class.java) // TODO this is built into aeron!??!?!?! - // TODO: this is for diffie hellmen handshake stuff! // serialization.register(IESParameters::class.java, IesParametersSerializer()) // serialization.register(IESWithCipherParameters::class.java, IesWithCipherParametersSerializer()) @@ -129,47 +113,15 @@ class Serialization(references: Boolean, // serialization.register(XECPrivateKey::class.java, XECPrivateKeySerializer()) serialization.register(dorkbox.network.connection.registration.Registration::class.java) // must use full package name! - - serialization.register(arrayListOf().javaClass) - serialization.register(hashMapOf().javaClass) - serialization.register(hashSetOf().javaClass) - - serialization.register(emptyList().javaClass) - serialization.register(emptySet().javaClass) - serialization.register(emptyMap().javaClass) - - serialization.register(Collections.EMPTY_LIST::class.java) - serialization.register(Collections.EMPTY_SET::class.java) - serialization.register(Collections.EMPTY_MAP::class.java) - serialization.register(Collections.emptyNavigableSet().javaClass) - serialization.register(Collections.emptyNavigableMap().javaClass) - - - // hacky way to register unmodifiable serializers - @Suppress("UNCHECKED_CAST") - val kryo: Kryo = object : Kryo() { - override fun register(type: Class<*>, serializer: Serializer<*>): Registration { - val type1 = type as Class - val serializer1 = serializer as Serializer - serialization.register(type1, serializer1) - - return super.register(type, serializer) - } - } - UnmodifiableCollectionsSerializer.registerSerializers(kryo) - // end hack - return serialization } - - // this prevents us from having to constantly do 'null' checks when serializing data - val NOP_CONNECTION: Connection_ = NopRmiConnection() } private lateinit var logger: Logger private var initialized = false private val kryoPool: ObjectPool + lateinit var classResolver: ClassResolver // used by operations performed during kryo initialization, which are by default package access (since it's an anon-inner class) // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. @@ -178,15 +130,26 @@ class Serialization(references: Boolean, private lateinit var savedRegistrationDetails: ByteArray /// RMI things + private val rmiIfaceToInstantiator : Int2ObjectHashMap> = Int2ObjectHashMap() private val rmiIfaceToImpl = IdentityMap, Class<*>>() private val rmiImplToIface = IdentityMap, Class<*>>() - private val remoteObjectSerializer = ObjectResponseSerializer(rmiImplToIface) + + + // BY DEFAULT, DefaultInstantiatorStrategy() will use ReflectASM + // StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK! + private val instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy()) + + private val methodRequestSerializer = MethodRequestSerializer() + private val methodResponseSerializer = MethodResponseSerializer() + private val objectRequestSerializer = RmiClientRequestSerializer() + private val objectResponseSerializer = ObjectResponseSerializer(rmiImplToIface) + + // the purpose of the method cache, is to accelerate looking up methods for specific class private val methodCache : Int2ObjectHashMap> = Int2ObjectHashMap() - // reflectASM doesn't work on android private val useAsm = !OS.isAndroid() @@ -196,33 +159,81 @@ class Serialization(references: Boolean, synchronized(this@Serialization) { // we HAVE to pre-allocate the KRYOs - val kryo = KryoExtra(this@Serialization) + val kryo = KryoExtra(methodCache) - - // BY DEFAULT, DefaultInstantiatorStrategy() will use ReflectASM - // StdInstantiatorStrategy will create classes bypasses the constructor (which can be useful in some cases) THIS IS A FALLBACK! - kryo.instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy()) + kryo.instantiatorStrategy = instantiatorStrategy kryo.references = references // All registration MUST happen in-order of when the register(*) method was called, otherwise there are problems. + // these are registered using the default serializers. We don't customize these, because we don't care about it. + kryo.register(String::class.java) + kryo.register(Array::class.java) + + kryo.register(IntArray::class.java) + kryo.register(ShortArray::class.java) + kryo.register(FloatArray::class.java) + kryo.register(DoubleArray::class.java) + kryo.register(LongArray::class.java) + kryo.register(ByteArray::class.java) + kryo.register(CharArray::class.java) + kryo.register(BooleanArray::class.java) + + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + kryo.register(Array::class.java) + + + kryo.register(Array::class.java) + kryo.register(Array>::class.java) + kryo.register(Class::class.java) + + // necessary for the transport of exceptions. + kryo.register(StackTraceElement::class.java) + kryo.register(Array::class.java) + + kryo.register(arrayListOf().javaClass) + kryo.register(hashMapOf().javaClass) + kryo.register(hashSetOf().javaClass) + + kryo.register(emptyList().javaClass) + kryo.register(emptySet().javaClass) + kryo.register(emptyMap().javaClass) + + kryo.register(Collections.EMPTY_LIST::class.java) + kryo.register(Collections.EMPTY_SET::class.java) + kryo.register(Collections.EMPTY_MAP::class.java) + kryo.register(Collections.emptyNavigableSet().javaClass) + kryo.register(Collections.emptyNavigableMap().javaClass) + + UNMODIFIABLE_COLLECTION_SERIALIZERS.forEach { + kryo.register(it.first, it.second) + } + + // RMI stuff! + kryo.register(GlobalObjectCreateRequest::class.java) + kryo.register(GlobalObjectCreateResponse::class.java) + + kryo.register(ConnectionObjectCreateRequest::class.java) + kryo.register(ConnectionObjectCreateResponse::class.java) + + kryo.register(MethodRequest::class.java, methodRequestSerializer) + kryo.register(MethodResponse::class.java, methodResponseSerializer) + + @Suppress("UNCHECKED_CAST") + kryo.register(InvocationHandler::class.java as Class, objectRequestSerializer) + + // check to see which interfaces are mapped to RMI (otherwise, the interface requires a serializer) classesToRegister.forEach { registration -> registration.register(kryo) } - // RMI stuff. This has to be for each kryo instance! - - // RMI stuff! - kryo.register(DynamicObjectRequest::class.java, DORequestSerializer()) - kryo.register(DynamicObjectResponse::class.java, DOResponseSerializer()) - kryo.register(MethodRequest::class.java, MethodRequestSerializer()) - kryo.register(MethodResponse::class.java, MethodResponseSerializer()) - - @Suppress("UNCHECKED_CAST") - kryo.register(InvocationHandler::class.java as Class, ObjectRequestSerializer(logger)) - - if (factory != null) { kryo.setDefaultSerializer(factory) } @@ -340,12 +351,7 @@ class Serialization(references: Boolean, /** * There is additional overhead to using RMI. * - * Specifically, It costs at least 2 bytes more to use remote method invocation than just sending the parameters. If the method has a - * return value which is not [ignored][dorkbox.network.rmi.RemoteObject.setAsync], an extra byte is written. - * If the type of a parameter is not final (primitives are final) then an extra byte is written for that parameter. - * - * - * Enable a "remote endpoint" to access methods and create objects (RMI) for this endpoint. + * This enables a "remote endpoint" to access methods and create objects (RMI) for this endpoint. * * This is NOT bi-directional, and this endpoint cannot access or create remote objects on the "remote client". * @@ -361,7 +367,7 @@ class Serialization(references: Boolean, require(ifaceClass.isInterface) { "Cannot register an implementation for RMI access. It must be an interface." } require(!implClass.isInterface) { "Cannot register an interface for RMI implementations. It must be an implementation." } - classesToRegister.add(ClassRegistrationIfaceAndImpl(ifaceClass, implClass, remoteObjectSerializer)) + classesToRegister.add(ClassRegistrationIfaceAndImpl(ifaceClass, implClass, objectResponseSerializer)) // rmiIfaceToImpl tells us, "the server" how to create a (requested) remote object // this MUST BE UNIQUE otherwise unexpected and BAD things can happen. @@ -390,6 +396,10 @@ class Serialization(references: Boolean, // initialize the kryo pool with at least 1 kryo instance. This ALSO makes sure that all of our class registration is done // correctly and (if not) we are are notified on the initial thread (instead of on the network update thread) val kryo = kryoPool.take() + // save off the class-resolver, so we can lookup the class <-> id relationships + classResolver = kryo.classResolver + + try { // now MERGE all of the registrations (since we can have registrations overwrite newer/specific registrations based on ID // in order to get the ID's, these have to be registered with a kryo instance! @@ -451,18 +461,27 @@ class Serialization(references: Boolean, // on the "RMI server" (aka, where the object lives) side, there will be an interface + implementation! methodCache[classRegistration.id] = RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, classRegistration.implClass, classRegistration.id) + + // we ALSO have to cache the instantiator for these, since these are used to create remote objects + val instantiator = kryo.instantiatorStrategy.newInstantiatorOf(classRegistration.implClass) + @Suppress("UNCHECKED_CAST") + rmiIfaceToInstantiator[classRegistration.id] = instantiator as ObjectInstantiator } else if (classRegistration.clazz.isInterface) { // on the "RMI client" methodCache[classRegistration.id] = RmiUtils.getCachedMethods(logger, kryo, useAsm, classRegistration.clazz, null, classRegistration.id) } + + if (classRegistration.id > 65000) { + throw RuntimeException("There are too many kryo class registrations!!") + } } // save this as a byte array (so class registration validation during connection handshake is faster) val buffer = Unpooled.buffer(CLASS_REGISTRATION_VALIDATION_FRAGMENT_SIZE) try { - kryo.writeCompressed(logger, NOP_CONNECTION, buffer, registrationDetails.toTypedArray()) + kryo.writeCompressed(logger, buffer, registrationDetails.toTypedArray()) } catch (e: Exception) { logger.error("Unable to write compressed data for registration details", e) } @@ -494,25 +513,22 @@ class Serialization(references: Boolean, * * @return true if kryo registration is required for all classes sent over the wire */ - override fun verifyKryoRegistration(bytes: ByteArray): Boolean { - val clientRegistrationData = bytes - + override fun verifyKryoRegistration(clientBytes: ByteArray): Boolean { // verify the registration IDs if necessary with our own. The CLIENT does not verify anything, only the server! val kryoRegistrationDetails = savedRegistrationDetails - val equals = kryoRegistrationDetails.contentEquals(clientRegistrationData) + val equals = kryoRegistrationDetails.contentEquals(clientBytes) if (equals) { return true } - // now we need to figure out WHAT was screwed up so we know what to fix // NOTE: it could just be that the byte arrays are different, because java has a non-deterministic iteration of hash maps. val kryo = takeKryo() - val byteBuf = Unpooled.wrappedBuffer(clientRegistrationData) + val byteBuf = Unpooled.wrappedBuffer(clientBytes) try { var success = true @Suppress("UNCHECKED_CAST") - val clientClassRegistrations = kryo.readCompressed(logger, NOP_CONNECTION, byteBuf, clientRegistrationData.size) as Array> + val clientClassRegistrations = kryo.readCompressed(logger, byteBuf, clientBytes.size) as Array> val lengthServer = classesToRegister.size val lengthClient = clientClassRegistrations.size var index = 0 @@ -579,6 +595,31 @@ class Serialization(references: Boolean, kryoPool.put(kryo) } + /** + * Returns the Kryo class registration ID + */ + override fun getClassId(iFace: Class<*>): Int { + return classResolver.getRegistration(iFace).id + } + + /** + * Returns the Kryo class from a registration ID + */ + override fun getClassFromId(interfaceClassId: Int): Class<*> { + return classResolver.getRegistration(interfaceClassId).type + } + + + /** + * Creates a NEW object implementation based on the KRYO interface ID. + * + * @return the corresponding implementation object + */ + override fun createRmiObject(interfaceClassId: Int): Any { + return rmiIfaceToInstantiator[interfaceClassId].newInstance() + } + + /** * Gets the RMI interface based on the specified implementation * diff --git a/test-orig/dorkbox/network/BaseTest.java b/test-orig/dorkbox/network/BaseTest.java deleted file mode 100644 index 68800eac..00000000 --- a/test-orig/dorkbox/network/BaseTest.java +++ /dev/null @@ -1,287 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network; - - -import static org.junit.Assert.fail; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.After; -import org.junit.Before; -import org.slf4j.LoggerFactory; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.encoder.PatternLayoutEncoder; -import ch.qos.logback.classic.joran.JoranConfigurator; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.ConsoleAppender; -import dorkbox.network.connection.EndPoint; -import dorkbox.util.entropy.Entropy; -import dorkbox.util.entropy.SimpleEntropy; -import dorkbox.util.exceptions.InitializationException; -import io.netty.util.ResourceLeakDetector; - -public abstract -class BaseTest { - - public static final String host = "127.0.0.1"; - public static final int tcpPort = 54558; - public static final int udpPort = 54779; - - // wait minimum of 2 minutes before we automatically fail the unit test. - public static final long AUTO_FAIL_TIMEOUT = 120; - - private final Object lock = new Object(); - private CountDownLatch latch = new CountDownLatch(1); - - private volatile Thread autoFailThread = null; - - static { - // disableAccessWarnings - try { - Class unsafeClass = Class.forName("sun.misc.Unsafe"); - Field field = unsafeClass.getDeclaredField("theUnsafe"); - field.setAccessible(true); - Object unsafe = field.get(null); - - Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile", Object.class, long.class, Object.class); - Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class); - - Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger"); - Field loggerField = loggerClass.getDeclaredField("logger"); - Long offset = (Long) staticFieldOffset.invoke(unsafe, loggerField); - putObjectVolatile.invoke(unsafe, loggerClass, offset, null); - } - catch (Exception ignored) { - } - - // we want our entropy generation to be simple (ie, no user interaction to generate) - try { - Entropy.init(SimpleEntropy.class); - } catch (InitializationException e) { - e.printStackTrace(); - } - } - - private final List endPointConnections = new CopyOnWriteArrayList(); - private volatile boolean isStopping = false; - - public - BaseTest() { - ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); - - System.out.println("---- " + getClass().getSimpleName()); - - // assume SLF4J is bound to logback in the current environment - Logger rootLogger = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - LoggerContext context = rootLogger.getLoggerContext(); - - JoranConfigurator jc = new JoranConfigurator(); - jc.setContext(context); - context.reset(); // override default configuration - -// rootLogger.setLevel(Level.OFF); - - // rootLogger.setLevel(Level.INFO); - rootLogger.setLevel(Level.DEBUG); - // rootLogger.setLevel(Level.TRACE); -// rootLogger.setLevel(Level.ALL); - - - // we only want error messages - Logger nettyLogger = (Logger) LoggerFactory.getLogger("io.netty"); - nettyLogger.setLevel(Level.ERROR); - - // we only want error messages - Logger kryoLogger = (Logger) LoggerFactory.getLogger("com.esotericsoftware"); - kryoLogger.setLevel(Level.ERROR); - - // we only want error messages - Logger barchartLogger = (Logger) LoggerFactory.getLogger("com.barchart"); - barchartLogger.setLevel(Level.ERROR); - - PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - encoder.setContext(context); - encoder.setPattern("%date{HH:mm:ss.SSS} %-5level [%logger{35}] %msg%n"); - encoder.start(); - - ConsoleAppender consoleAppender = new ch.qos.logback.core.ConsoleAppender(); - - consoleAppender.setContext(context); - consoleAppender.setEncoder(encoder); - consoleAppender.start(); - - rootLogger.addAppender(consoleAppender); - } - - public - void addEndPoint(final EndPoint endPointConnection) { - this.endPointConnections.add(endPointConnection); - synchronized (lock) { - latch = new CountDownLatch(endPointConnections.size() + 1); - } - } - - /** - * Immediately stop the endpoints - */ - public - void stopEndPoints() { - stopEndPoints(0); - } - - public - void stopEndPoints(final long stopAfterMillis) { - ThreadGroup threadGroup = Thread.currentThread() - .getThreadGroup(); - final String name = threadGroup.getName(); - - // if (name.contains(THREADGROUP_NAME)) { - // // We have to ALWAYS run this in a new thread, BECAUSE if stopEndPoints() is called from a client/server thread, it will DEADLOCK - // final Thread thread = new Thread(threadGroup.getParent(), new Runnable() { - // @Override - // public - // void run() { - // stopEndPoints_(stopAfterMillis); - // } - // }, "UnitTest shutdown"); // a different name for the thread - // - // thread.setDaemon(true); - // thread.start(); - // } else { - stopEndPoints_(stopAfterMillis); - // } - } - - private - void stopEndPoints_(final long stopAfterMillis) { - if (isStopping) { - return; - } - - isStopping = true; - - // not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we - // ARE NOT in the same thread group as netty! - try { - Thread.sleep(stopAfterMillis); - } catch (InterruptedException ignored) { - } - - synchronized (lock) { - } - - // shutdown clients first - for (EndPoint endPoint : this.endPointConnections) { - if (endPoint.getType() == Client.class) { - endPoint.stop(); - endPoint.waitForShutdown(); - - latch.countDown(); - } - } - // shutdown servers last - for (EndPoint endPoint : this.endPointConnections) { - if (endPoint.getType() == Server.class) { - endPoint.stop(); - endPoint.waitForShutdown(); - - latch.countDown(); - } - } - - // we start with "1", so make sure to end it - latch.countDown(); - this.endPointConnections.clear(); - } - - /** - * Wait for network client/server threads to shutdown on their own, BUT WILL ERROR (+ shutdown) if they take longer than 2 minutes. - */ - public - void waitForThreads() { - waitForThreads(AUTO_FAIL_TIMEOUT/2); - } - - /** - * Wait for network client/server threads to shutdown for the specified time. - * - * @param stopAfterSeconds how many seconds to wait - */ - public - void waitForThreads(long stopAfterSeconds) { - synchronized (lock) { - } - - try { - latch.await(stopAfterSeconds, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Before - public - void setupFailureCheck() { - autoFailThread = new Thread(new Runnable() { - @Override - public - void run() { - // not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we - // ARE NOT in the same thread group as netty! - try { - Thread.sleep(AUTO_FAIL_TIMEOUT * 1000L); - - // if the thread is interrupted, then it means we finished the test. - System.err.println("Test did not complete in a timely manner..."); - - stopEndPoints(0L); - fail("Test did not complete in a timely manner."); - } catch (InterruptedException ignored) { - } - } - }, "UnitTest timeout fail condition"); - autoFailThread.setDaemon(true); - // autoFailThread.start(); - } - - @After - public - void cancelFailureCheck() { - if (autoFailThread != null) { - autoFailThread.interrupt(); - autoFailThread = null; - } - - // Give sockets a chance to close before starting the next test. - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - } -} diff --git a/test-orig/dorkbox/network/ListenerTest.java b/test-orig/dorkbox/network/ListenerTest.java deleted file mode 100644 index e0bb073f..00000000 --- a/test-orig/dorkbox/network/ListenerTest.java +++ /dev/null @@ -1,294 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.ConnectionImpl; -import dorkbox.network.connection.EndPoint; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.network.connection.wrapper.ChannelWrapper; -import dorkbox.util.exceptions.InitializationException; -import dorkbox.util.exceptions.SecurityException; - -public -class ListenerTest extends BaseTest { - - private final String origString = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; // lots of a's to encourage compression - private final int limit = 20; - private AtomicInteger count = new AtomicInteger(0); - - AtomicBoolean checkFail1 = new AtomicBoolean(false); - AtomicBoolean checkFail2 = new AtomicBoolean(false); - - AtomicBoolean check1 = new AtomicBoolean(false); - AtomicBoolean check2 = new AtomicBoolean(false); - AtomicBoolean check3 = new AtomicBoolean(false); - AtomicBoolean check4 = new AtomicBoolean(false); - AtomicBoolean check5 = new AtomicBoolean(false); - AtomicBoolean check6 = new AtomicBoolean(false); - AtomicBoolean check7 = new AtomicBoolean(false); - AtomicBoolean check8 = new AtomicBoolean(false); - AtomicBoolean check9 = new AtomicBoolean(false); - - - // quick and dirty test to also test connection sub-classing - class TestConnectionA extends ConnectionImpl { - public - TestConnectionA(final EndPoint endPointConnection, final ChannelWrapper wrapper) { - super(endPointConnection, wrapper); - } - - public - void check() { - ListenerTest.this.check1.set(true); - } - } - - - class TestConnectionB extends TestConnectionA { - public - TestConnectionB(final EndPoint endPointConnection, final ChannelWrapper wrapper) { - super(endPointConnection, wrapper); - } - - @Override - public - void check() { - ListenerTest.this.checkFail1.set(true); - } - } - - abstract class SubListener implements Listener.OnMessageReceived { - } - - abstract class SubListener2 extends SubListener { - } - - abstract class SubListener3 implements Listener.OnMessageReceived, Listener.SelfDefinedType { - } - - - @SuppressWarnings("rawtypes") - @Test - public - void listener() throws SecurityException, InitializationException, IOException, InterruptedException { - Configuration configuration = new Configuration(); - configuration.tcpPort = tcpPort; - configuration.host = host; - - Server server = new Server(configuration) { - @Override - public - TestConnectionA newConnection(final EndPoint endPoint, final ChannelWrapper wrapper) { - return new TestConnectionA(endPoint, wrapper); - } - }; - - addEndPoint(server); - server.bind(false); - final Listeners listeners = server.listeners(); - - // standard listener - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(TestConnectionA connection, String string) { - connection.check(); - connection.send() - .TCP(string); - } - }); - - // standard listener with connection subclassed - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String string) { - ListenerTest.this.check2.set(true); - } - }); - - // standard listener with message subclassed - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(TestConnectionA connection, Object string) { - ListenerTest.this.check3.set(true); - } - }); - - // standard listener with connection subclassed AND message subclassed - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, Object string) { - ListenerTest.this.check4.set(true); - } - }); - - // standard listener with connection subclassed AND message subclassed NO GENERICS - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, Object string) { - ListenerTest.this.check5.set(true); - } - }); - - // subclassed listener with connection subclassed AND message subclassed NO GENERICS - listeners.add(new SubListener() { - @Override - public - void received(Connection connection, String string) { - ListenerTest.this.check6.set(true); - } - }); - - // subclassed listener with connection subclassed AND message subclassed NO GENERICS - listeners.add(new SubListener() { - @Override - public - void received(Connection connection, String string) { - ListenerTest.this.check6.set(true); - } - }); - - - // subclassed listener with connection subclassed x 2 AND message subclassed NO GENERICS - listeners.add(new SubListener2() { - @Override - public - void received(Connection connection, String string) { - ListenerTest.this.check8.set(true); - } - }); - - - // subclassed listener with connection subclassed AND message subclassed NO GENERICS - listeners.add(new SubListener3() { - @Override - public - Class getType() { - return String.class; - } - - @Override - public - void received(Connection connection, String string) { - ListenerTest.this.check9.set(true); - } - }); - - - // standard listener disconnect check - listeners.add(new Listener.OnDisconnected() { - @Override - public - void disconnected(Connection connection) { - ListenerTest.this.check7.set(true); - } - }); - - - // should not let this happen! - try { - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(TestConnectionB connection, String string) { - connection.check(); - System.err.println(string); - connection.TCP(string); - } - }); - fail("Should not be able to ADD listeners that are NOT the basetype or the interface"); - } catch (Exception e) { - System.err.println("Successfully did NOT add listener that was not the base class"); - } - - - // ---- - - Client client = new Client(configuration); - - addEndPoint(client); - client.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - connection.send() - .TCP(ListenerTest.this.origString); // 20 a's - } - }); - - client.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String string) { - if (ListenerTest.this.count.getAndIncrement() < ListenerTest.this.limit) { - connection.send() - .TCP(string); - } - else { - if (!ListenerTest.this.origString.equals(string)) { - checkFail2.set(true); - System.err.println("original string not equal to the string received"); - } - stopEndPoints(); - } - } - }); - - - client.connect(5000); - - waitForThreads(); - - // -1 BECAUSE we are `getAndIncrement` for each check earlier - assertEquals(this.limit, this.count.get()-1); - - assertTrue(this.check1.get()); - assertTrue(this.check2.get()); - assertTrue(this.check3.get()); - assertTrue(this.check4.get()); - assertTrue(this.check5.get()); - assertTrue(this.check6.get()); - assertTrue(this.check7.get()); - assertTrue(this.check8.get()); - assertTrue(this.check9.get()); - - assertFalse(this.checkFail1.get()); - assertFalse(this.checkFail2.get()); - } -} diff --git a/test-orig/dorkbox/network/MultipleServerTest.java b/test-orig/dorkbox/network/MultipleServerTest.java deleted file mode 100644 index c042d0c7..00000000 --- a/test-orig/dorkbox/network/MultipleServerTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network; - -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.Listener; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; - -public -class MultipleServerTest extends BaseTest { - AtomicInteger received = new AtomicInteger(); - - @Test - public - void multipleServers() throws SecurityException, IOException { - Configuration configuration1 = new Configuration(); - configuration1.tcpPort = tcpPort; - configuration1.udpPort = udpPort; - configuration1.localChannelName = "chan1"; - configuration1.serialization = Serialization.DEFAULT(); - configuration1.serialization.register(String[].class); - - Server server1 = new Server(configuration1); - addEndPoint(server1); - - server1.bind(false); - server1.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - if (!object.equals("client1")) { - fail(); - } - if (MultipleServerTest.this.received.incrementAndGet() == 2) { - stopEndPoints(); - } - } - }); - - Configuration configuration2 = new Configuration(); - configuration2.tcpPort = tcpPort + 1; - configuration2.udpPort = udpPort + 1; - configuration2.localChannelName = "chan2"; - configuration2.serialization = Serialization.DEFAULT(); - configuration2.serialization.register(String[].class); - - Server server2 = new Server(configuration2); - - addEndPoint(server2); - server2.bind(false); - server2.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, String object) { - if (!object.equals("client2")) { - fail(); - } - if (MultipleServerTest.this.received.incrementAndGet() == 2) { - stopEndPoints(); - } - } - }); - - // ---- - - configuration1.localChannelName = null; - configuration1.host = host; - - - Client client1 = new Client(configuration1); - addEndPoint(client1); - client1.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - connection.send() - .TCP("client1"); - } - }); - client1.connect(5000); - - - configuration2.localChannelName = null; - configuration2.host = host; - - Client client2 = new Client(configuration2); - addEndPoint(client2); - client2.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - connection.send() - .TCP("client2"); - } - }); - client2.connect(5000); - - waitForThreads(30); - } -} diff --git a/test-orig/dorkbox/network/PingPongTest.java b/test-orig/dorkbox/network/PingPongTest.java deleted file mode 100644 index 71b249ed..00000000 --- a/test-orig/dorkbox/network/PingPongTest.java +++ /dev/null @@ -1,419 +0,0 @@ -/* Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network; - -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.EndPoint; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; -import dorkbox.util.serialization.SerializationManager; - -public -class PingPongTest extends BaseTest { - private volatile String fail; - - int tries = 1000; - - - enum TYPE { - TCP, UDP - } - - @Test - public - void pingPong() throws SecurityException, IOException { - // UDP data is kinda big. Make sure it fits into one packet. - int origSize = EndPoint.udpMaxSize; - EndPoint.udpMaxSize = 2048; - - this.fail = "Data not received."; - - Configuration configuration = new Configuration(); - configuration.tcpPort = tcpPort; - configuration.udpPort = udpPort; - configuration.host = host; - configuration.serialization = Serialization.DEFAULT(); - register(configuration.serialization); - - - final Data dataTCP = new Data(); - populateData(dataTCP, TYPE.TCP); - final Data dataUDP = new Data(); - populateDataTiny(dataUDP, TYPE.UDP); // UDP has a max size it can send! - - Server server = new Server(configuration); - addEndPoint(server); - server.bind(false); - final Listeners listeners1 = server.listeners(); - listeners1.add(new Listener.OnError() { - @Override - public - void error(Connection connection, Throwable throwable) { - PingPongTest.this.fail = "Error during processing. " + throwable; - } - }); - - listeners1.add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, Data data) { - if (data.type == TYPE.TCP) { - if (!data.equals(dataTCP)) { - PingPongTest.this.fail = "TCP data is not equal on server."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - connection.send() - .TCP(dataTCP); - } - else if (data.type == TYPE.UDP) { - if (!data.equals(dataUDP)) { - PingPongTest.this.fail = "UDP data is not equal on server."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - connection.send() - .UDP(dataUDP); - } - else { - PingPongTest.this.fail = "Unknown data type on server."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - } - }); - - // ---- - - Client client = new Client(configuration); - addEndPoint(client); - final Listeners listeners = client.listeners(); - listeners.add(new Listener.OnConnected() { - @Override - public - void connected(Connection connection) { - PingPongTest.this.fail = null; - connection.send() - .TCP(dataTCP); - connection.send() - .UDP(dataUDP); // UDP ping pong stops if a UDP packet is lost. - } - }); - - listeners.add(new Listener.OnError() { - @Override - public - void error(Connection connection, Throwable throwable) { - PingPongTest.this.fail = "Error during processing. " + throwable; - throwable.printStackTrace(); - } - }); - - listeners.add(new Listener.OnMessageReceived() { - AtomicInteger checkTCP = new AtomicInteger(0); - AtomicInteger checkUDP = new AtomicInteger(0); - AtomicBoolean doneTCP = new AtomicBoolean(false); - AtomicBoolean doneUDP = new AtomicBoolean(false); - - @Override - public - void received(Connection connection, Data data) { - if (data.type == TYPE.TCP) { - if (!data.equals(dataTCP)) { - PingPongTest.this.fail = "TCP data is not equal on client."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - if (this.checkTCP.getAndIncrement() <= PingPongTest.this.tries) { - connection.send() - .TCP(dataTCP); - } - else { - System.err.println("TCP done."); - this.doneTCP.set(true); - } - } - else if (data.type == TYPE.UDP) { - if (!data.equals(dataUDP)) { - PingPongTest.this.fail = "UDP data is not equal on client."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - if (this.checkUDP.getAndIncrement() <= PingPongTest.this.tries) { - connection.send() - .UDP(dataUDP); - } - else { - System.err.println("UDP done."); - this.doneUDP.set(true); - } - } - else { - PingPongTest.this.fail = "Unknown data type on client."; - throw new RuntimeException("Fail! " + PingPongTest.this.fail); - } - - if (this.doneTCP.get() && this.doneUDP.get()) { - System.err.println("Ran TCP, UDP " + PingPongTest.this.tries + " times each"); - stopEndPoints(); - } - } - }); - - client.connect(5000); - - waitForThreads(); - - if (this.fail != null) { - fail(this.fail); - } - - EndPoint.udpMaxSize = origSize; - } - - private static - void populateData(Data data, TYPE type) { - data.type = type; - - StringBuilder buffer = new StringBuilder(3001); - for (int i = 0; i < 3000; i++) { - buffer.append('a'); - } - data.string = buffer.toString(); - - data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789", "", null, "!@#$", "�����"}; - data.ints = new int[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE}; - data.shorts = new short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE}; - data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE, Float.MIN_VALUE}; - - data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE}; - data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE}; - data.bytes = new byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE}; - data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE}; - - data.booleans = new boolean[] {true, false}; - data.Ints = new Integer[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE}; - data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE}; - data.Floats = new Float[] {0f, -0f, 1.0f, -1f, 123456f, -123456f, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE, - Float.MIN_VALUE}; - data.Doubles = new Double[] {0d, -0d, 1d, -1d, 123456d, -123456d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE}; - data.Longs = new Long[] {0l, -0l, 1l, -1l, 123456l, -123456l, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE}; - data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE}; - data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE}; - data.Booleans = new Boolean[] {true, false}; - } - - - // ONLY for UDP, since there is a 508 byte hard limit to UDP packets! - private static - void populateDataTiny(Data data, TYPE type) { - data.type = type; - - StringBuilder buffer = new StringBuilder(10); - for (int i = 0; i < 10; i++) { - buffer.append('a'); - } - data.string = buffer.toString(); - - data.strings = new String[] {"abcdefghijklmnopqrstuvwxyz0123456789", "", null, "!@#$", "�����"}; - data.ints = new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE}; - data.shorts = new short[] {Short.MAX_VALUE, Short.MIN_VALUE}; - data.floats = new float[] {Float.MAX_VALUE, Float.MIN_VALUE}; - - data.doubles = new double[] {Double.MAX_VALUE, Double.MIN_VALUE}; - data.longs = new long[] {Long.MAX_VALUE, Long.MIN_VALUE}; - data.bytes = new byte[] {Byte.MAX_VALUE, Byte.MIN_VALUE}; - data.chars = new char[] {Character.MAX_VALUE, Character.MIN_VALUE}; - - data.booleans = new boolean[] {true, false}; - data.Ints = new Integer[] {Integer.MAX_VALUE, Integer.MIN_VALUE}; - data.Shorts = new Short[] {Short.MAX_VALUE, Short.MIN_VALUE}; - data.Floats = new Float[] {Float.MAX_VALUE, Float.MIN_VALUE}; - data.Doubles = new Double[] {Double.MAX_VALUE, Double.MIN_VALUE}; - data.Longs = new Long[] {Long.MAX_VALUE, Long.MIN_VALUE}; - data.Bytes = new Byte[] {Byte.MAX_VALUE, Byte.MIN_VALUE}; - data.Chars = new Character[] {Character.MAX_VALUE, Character.MIN_VALUE}; - data.Booleans = new Boolean[] {true, false}; - } - - private - void register(SerializationManager manager) { - manager.register(int[].class); - manager.register(short[].class); - manager.register(float[].class); - manager.register(double[].class); - manager.register(long[].class); - manager.register(byte[].class); - manager.register(char[].class); - manager.register(boolean[].class); - manager.register(String[].class); - manager.register(Integer[].class); - manager.register(Short[].class); - manager.register(Float[].class); - manager.register(Double[].class); - manager.register(Long[].class); - manager.register(Byte[].class); - manager.register(Character[].class); - manager.register(Boolean[].class); - manager.register(Data.class); - manager.register(TYPE.class); - } - - public static - class Data { - public TYPE type; - public String string; - public String[] strings; - public int[] ints; - public short[] shorts; - public float[] floats; - public double[] doubles; - public long[] longs; - public byte[] bytes; - public char[] chars; - public boolean[] booleans; - public Integer[] Ints; - public Short[] Shorts; - public Float[] Floats; - public Double[] Doubles; - public Long[] Longs; - public Byte[] Bytes; - public Character[] Chars; - public Boolean[] Booleans; - - - @Override - public - int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(this.Booleans); - result = prime * result + Arrays.hashCode(this.Bytes); - result = prime * result + Arrays.hashCode(this.Chars); - result = prime * result + Arrays.hashCode(this.Doubles); - result = prime * result + Arrays.hashCode(this.Floats); - result = prime * result + Arrays.hashCode(this.Ints); - result = prime * result + Arrays.hashCode(this.Longs); - result = prime * result + Arrays.hashCode(this.Shorts); - result = prime * result + Arrays.hashCode(this.booleans); - result = prime * result + Arrays.hashCode(this.bytes); - result = prime * result + Arrays.hashCode(this.chars); - result = prime * result + Arrays.hashCode(this.doubles); - result = prime * result + Arrays.hashCode(this.floats); - result = prime * result + Arrays.hashCode(this.ints); - result = prime * result + Arrays.hashCode(this.longs); - result = prime * result + Arrays.hashCode(this.shorts); - result = prime * result + (this.string == null ? 0 : this.string.hashCode()); - result = prime * result + Arrays.hashCode(this.strings); - result = prime * result + (this.type == null ? 0 : this.type.hashCode()); - return result; - } - - @Override - public - boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Data other = (Data) obj; - if (!Arrays.equals(this.Booleans, other.Booleans)) { - return false; - } - if (!Arrays.equals(this.Bytes, other.Bytes)) { - return false; - } - if (!Arrays.equals(this.Chars, other.Chars)) { - return false; - } - if (!Arrays.equals(this.Doubles, other.Doubles)) { - return false; - } - if (!Arrays.equals(this.Floats, other.Floats)) { - return false; - } - if (!Arrays.equals(this.Ints, other.Ints)) { - return false; - } - if (!Arrays.equals(this.Longs, other.Longs)) { - return false; - } - if (!Arrays.equals(this.Shorts, other.Shorts)) { - return false; - } - if (!Arrays.equals(this.booleans, other.booleans)) { - return false; - } - if (!Arrays.equals(this.bytes, other.bytes)) { - return false; - } - if (!Arrays.equals(this.chars, other.chars)) { - return false; - } - if (!Arrays.equals(this.doubles, other.doubles)) { - return false; - } - if (!Arrays.equals(this.floats, other.floats)) { - return false; - } - if (!Arrays.equals(this.ints, other.ints)) { - return false; - } - if (!Arrays.equals(this.longs, other.longs)) { - return false; - } - if (!Arrays.equals(this.shorts, other.shorts)) { - return false; - } - if (this.string == null) { - if (other.string != null) { - return false; - } - } - else if (!this.string.equals(other.string)) { - return false; - } - if (!Arrays.equals(this.strings, other.strings)) { - return false; - } - if (this.type != other.type) { - return false; - } - return true; - } - - @Override - public - String toString() { - return "Data"; - } - } -} diff --git a/test-orig/dorkbox/network/rmi/Config.java b/test-orig/dorkbox/network/rmi/Config.java deleted file mode 100644 index aa2f3a42..00000000 --- a/test-orig/dorkbox/network/rmi/Config.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2018 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.Configuration; - -public -interface Config { - void apply(Configuration configuration); -} diff --git a/test-orig/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java b/test-orig/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java deleted file mode 100644 index 25fa0901..00000000 --- a/test-orig/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2016 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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.BaseTest; -import dorkbox.network.Client; -import dorkbox.network.Configuration; -import dorkbox.network.Server; -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.EndPoint; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.bridge.ConnectionBridge; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; - -@SuppressWarnings("Duplicates") -public -class RmiSendObjectOverrideMethodTest extends BaseTest { - - @Test - public - void rmiTcp() throws SecurityException, IOException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.tcpPort = tcpPort; - configuration.host = host; - } - }); - } - - @Test - public - void rmiUdp() throws SecurityException, IOException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.udpPort = udpPort; - configuration.host = host; - } - }); - } - - @Test - public - void rmiLocal() throws SecurityException, IOException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.localChannelName = EndPoint.LOCAL_CHANNEL; - } - }); - } - - /** - * In this test the server has two objects in an object space. The client - * uses the first remote object to get the second remote object. - * - * - * The MAJOR difference in this version, is that we use an interface to override the methods, so that we can have the RMI system pass - * in the connection object. - * - * Specifically, from CachedMethod.java - * - * In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B. - * This is to support calling RMI methods from an interface (that does pass the connection reference) to - * an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use - * the interface, and the implType may override the method, so that we add the connection as the first in - * the list of parameters. - * - * for example: - * Interface: foo(String x) - * Impl: foo(Connection c, String x) - * - * The implType (if it exists, with the same name, and with the same signature + connection parameter) will be called from the interface - * instead of the method that would NORMALLY be called. - */ - public - void rmi(final Config config) throws SecurityException, IOException { - Configuration configuration = new Configuration(); - config.apply(configuration); - - final boolean isUDP = configuration.udpPort > 0; - - configuration.serialization = Serialization.DEFAULT(); - configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class); - configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class); - configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire - - Server server = new Server(configuration); - server.setIdleTimeout(0); - - - addEndPoint(server); - server.bind(false); - - server.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, OtherObjectImpl object) { - // The test is complete when the client sends the OtherObject instance. - - // this 'object' is the REAL object, not a proxy, because this object is created within this connection. - if (object.value() == 12.34f) { - stopEndPoints(); - } else { - fail("Incorrect object value"); - } - } - }); - - - // ---- - configuration = new Configuration(); - config.apply(configuration); - - configuration.serialization = Serialization.DEFAULT(); - configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class); - configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class); - configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire - - - - Client client = new Client(configuration); - client.setIdleTimeout(0); - - addEndPoint(client); - client.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(final Connection connection) { - // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestObject.class, new RemoteObjectCallback() { - @Override - public - void created(final TestObject remoteObject) { - // MUST run on a separate thread because remote object method invocations are blocking - new Thread() { - @Override - public - void run() { - remoteObject.setOther(43.21f); - - // Normal remote method call. - assertEquals(43.21f, remoteObject.other(), .0001f); - - // Make a remote method call that returns another remote proxy object. - // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created. - // here we have a proxy to both of them. - OtherObject otherObject = remoteObject.getOtherObject(); - - // Normal remote method call on the second object. - otherObject.setValue(12.34f); - - float value = otherObject.value(); - assertEquals(12.34f, value, .0001f); - - // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because - // that is where that object acutally exists. - // we have to manually flush, since we are in a separate thread that does not auto-flush. - ConnectionBridge send = connection.send(); - - if (isUDP) { - send.UDP(otherObject) - .flush(); - } else { - send.TCP(otherObject) - .flush(); - } - } - }.start(); - } - }); - } - }); - - client.connect(5000); - - waitForThreads(); - } - - private - interface TestObject { - void setOther(float aFloat); - - float other(); - - OtherObject getOtherObject(); - } - - - private - interface OtherObject { - void setValue(float aFloat); - float value(); - } - - - private static final AtomicInteger idCounter = new AtomicInteger(); - - - private static - class TestObjectImpl implements TestObject { - private final transient int ID = idCounter.getAndIncrement(); - - @Rmi - private final OtherObject otherObject = new OtherObjectImpl(); - private float aFloat; - - - @Override - public - void setOther(final float aFloat) { - throw new RuntimeException("Whoops!"); - } - - public - void setOther(Connection connection, final float aFloat) { - this.aFloat = aFloat; - } - - @Override - public - float other() { - throw new RuntimeException("Whoops!"); - } - - public - float other(Connection connection) { - return aFloat; - } - - @Override - public - OtherObject getOtherObject() { - throw new RuntimeException("Whoops!"); - } - - public - OtherObject getOtherObject(Connection connection) { - return this.otherObject; - } - - @Override - public - int hashCode() { - return ID; - } - } - - - private static - class OtherObjectImpl implements OtherObject { - private final transient int ID = idCounter.getAndIncrement(); - - private float aFloat; - - @Override - public - void setValue(final float aFloat) { - this.aFloat = aFloat; - } - - @Override - public - float value() { - return aFloat; - } - - @Override - public - int hashCode() { - return ID; - } - } -} diff --git a/test-orig/dorkbox/network/rmi/RmiSendObjectTest.java b/test-orig/dorkbox/network/rmi/RmiSendObjectTest.java deleted file mode 100644 index f505136c..00000000 --- a/test-orig/dorkbox/network/rmi/RmiSendObjectTest.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2016 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network.rmi; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import dorkbox.network.BaseTest; -import dorkbox.network.Client; -import dorkbox.network.Configuration; -import dorkbox.network.Server; -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.EndPoint; -import dorkbox.network.connection.Listener; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; - -@SuppressWarnings("Duplicates") -public -class RmiSendObjectTest extends BaseTest { - - @Test - public - void rmiNetwork() throws SecurityException, IOException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.tcpPort = tcpPort; - configuration.host = host; - } - }); - } - - @Test - public - void rmiLocal() throws SecurityException, IOException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.localChannelName = EndPoint.LOCAL_CHANNEL; - } - }); - } - - /** - * In this test the server has two objects in an object space. The client - * uses the first remote object to get the second remote object. - */ - public - void rmi(final Config config) throws SecurityException, IOException { - Configuration configuration = new Configuration(); - config.apply(configuration); - - configuration.serialization = Serialization.DEFAULT(); - configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class); - configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class); - configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire - - - - - - Server server = new Server(configuration); - server.setIdleTimeout(0); - - - addEndPoint(server); - server.bind(false); - - - server.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, OtherObjectImpl object) { - // The test is complete when the client sends the OtherObject instance. - if (object.value() == 12.34F) { - stopEndPoints(); - } else { - fail("Incorrect object value"); - } - } - }); - - - // ---- - configuration = new Configuration(); - config.apply(configuration); - - configuration.serialization = Serialization.DEFAULT(); - configuration.serialization.registerRmi(TestObject.class, TestObjectImpl.class); - configuration.serialization.registerRmi(OtherObject.class, OtherObjectImpl.class); - configuration.serialization.register(OtherObjectImpl.class); // registered because this class is sent over the wire - - - Client client = new Client(configuration); - client.setIdleTimeout(0); - - addEndPoint(client); - client.listeners() - .add(new Listener.OnConnected() { - @Override - public - void connected(final Connection connection) { - connection.createRemoteObject(TestObject.class, new RemoteObjectCallback() { - @Override - public - void created(final TestObject remoteObject) { - // MUST run on a separate thread because remote object method invocations are blocking - new Thread() { - @Override - public - void run() { - remoteObject.setOther(43.21f); - - // Normal remote method call. - assertEquals(43.21f, remoteObject.other(), 0.0001F); - - // Make a remote method call that returns another remote proxy object. - OtherObject otherObject = remoteObject.getOtherObject(); - - // Normal remote method call on the second object. - otherObject.setValue(12.34f); - float value = otherObject.value(); - assertEquals(12.34f, value, 0.0001F); - - // When a remote proxy object is sent, the other side receives its actual remote object. - // we have to manually flush, since we are in a separate thread that does not auto-flush. - connection.send() - .TCP(otherObject) - .flush(); - } - }.start(); - } - }); - } - }); - - client.connect(0); - - waitForThreads(); - } - - private - interface TestObject { - void setOther(float aFloat); - float other(); - OtherObject getOtherObject(); - } - - - private - interface OtherObject { - void setValue(float aFloat); - float value(); - } - - - private static final AtomicInteger idCounter = new AtomicInteger(); - - private static - class TestObjectImpl implements TestObject { - private final transient int ID = idCounter.getAndIncrement(); - - @Rmi - private final OtherObject otherObject = new OtherObjectImpl(); - private float aFloat; - - - @Override - public - void setOther(final float aFloat) { - this.aFloat = aFloat; - } - - @Override - public - float other() { - return aFloat; - } - - @Override - public - OtherObject getOtherObject() { - return this.otherObject; - } - - @Override - public - int hashCode() { - return ID; - } - } - - - private static - class OtherObjectImpl implements OtherObject { - private final transient int ID = idCounter.getAndIncrement(); - - private float aFloat; - - @Override - public - void setValue(final float aFloat) { - this.aFloat = aFloat; - } - - @Override - public - float value() { - return aFloat; - } - - @Override - public - int hashCode() { - return ID; - } - } -} diff --git a/test-orig/dorkbox/network/rmi/RmiTest.java b/test-orig/dorkbox/network/rmi/RmiTest.java deleted file mode 100644 index 2185c213..00000000 --- a/test-orig/dorkbox/network/rmi/RmiTest.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright 2016 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. - * - * Copyright (c) 2008, Nathan Sweet - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided with the distribution. - * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * 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. - */ -package dorkbox.network.rmi; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; - -import org.junit.Test; - -import dorkbox.network.BaseTest; -import dorkbox.network.Client; -import dorkbox.network.Configuration; -import dorkbox.network.Server; -import dorkbox.network.connection.Connection; -import dorkbox.network.connection.EndPoint; -import dorkbox.network.connection.Listener; -import dorkbox.network.connection.Listeners; -import dorkbox.network.serialization.NetworkSerializationManager; -import dorkbox.network.serialization.Serialization; -import dorkbox.util.exceptions.SecurityException; - -public -class RmiTest extends BaseTest { - - public static - void runTests(final Connection connection, final TestCow test, final int remoteObjectID) { - RemoteObject remoteObject = (RemoteObject) test; - - // Default behavior. RMI is transparent, method calls behave like normal - // (return values and exceptions are returned, call is synchronous) - System.err.println("hashCode: " + test.hashCode()); - System.err.println("toString: " + test.toString()); - - // see what the "remote" toString() method is - final String s = remoteObject.toString(); - remoteObject.enableToString(true); - assertFalse(s.equals(remoteObject.toString())); - - test.moo(); - test.moo("Cow"); - assertEquals(remoteObjectID, test.id()); - - - // UDP calls that ignore the return value - remoteObject.setUDP(); - remoteObject.setAsync(true); - remoteObject.setTransmitReturnValue(false); - remoteObject.setTransmitExceptions(false); - test.moo("Meow"); - assertEquals(0, test.id()); - - remoteObject.setAsync(false); - remoteObject.setTransmitReturnValue(true); - remoteObject.setTransmitExceptions(true); - remoteObject.setTCP(); - - - // Test that RMI correctly waits for the remotely invoked method to exit - remoteObject.setResponseTimeout(5000); - test.moo("You should see this two seconds before...", 2000); - System.out.println("...This"); - remoteObject.setResponseTimeout(3000); - - // Try exception handling - boolean caught = false; - try { - test.throwException(); - } catch (UnsupportedOperationException ex) { - System.err.println("\tExpected."); - caught = true; - } - assertTrue(caught); - - - // Return values are ignored, but exceptions are still dealt with properly - remoteObject.setTransmitReturnValue(false); - test.moo("Baa"); - test.id(); - caught = false; - try { - test.throwException(); - } catch (UnsupportedOperationException ex) { - caught = true; - } - assertTrue(caught); - - // Non-blocking call that ignores the return value - remoteObject.setAsync(true); - remoteObject.setTransmitReturnValue(false); - test.moo("Meow"); - assertEquals(0, test.id()); - - // Non-blocking call that returns the return value - remoteObject.setTransmitReturnValue(true); - test.moo("Foo"); - - assertEquals(0, test.id()); - // wait for the response to id() - assertEquals(remoteObjectID, remoteObject.waitForLastResponse()); - - assertEquals(0, test.id()); - byte responseID = remoteObject.getLastResponseID(); - // wait for the response to id() - assertEquals(remoteObjectID, remoteObject.waitForResponse(responseID)); - - // Non-blocking call that errors out - remoteObject.setTransmitReturnValue(false); - test.throwException(); - assertEquals(remoteObject.waitForLastResponse() - .getClass(), UnsupportedOperationException.class); - - // Call will time out if non-blocking isn't working properly - remoteObject.setTransmitExceptions(false); - test.moo("Mooooooooo", 3000); - - // should wait for a small time - remoteObject.setTransmitReturnValue(true); - remoteObject.setAsync(false); - remoteObject.setResponseTimeout(6000); - System.out.println("You should see this 2 seconds before"); - float slow = test.slow(); - System.out.println("...This"); - assertEquals(slow, 123, 0.0001D); - - - // Test sending a reference to a remote object. - MessageWithTestCow m = new MessageWithTestCow(test); - m.number = 678; - m.text = "sometext"; - connection.send() - .TCP(m) - .flush(); - - System.out.println("Finished tests"); - } - - public static - void register(NetworkSerializationManager manager) { - manager.register(Object.class); // Needed for Object#toString, hashCode, etc. - manager.register(TestCow.class); - manager.register(MessageWithTestCow.class); - manager.register(UnsupportedOperationException.class); - } - - @Test - public - void rmiNetwork() throws SecurityException, IOException, InterruptedException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.tcpPort = tcpPort; - configuration.udpPort = udpPort; - configuration.host = host; - } - }); - - // have to reset the object ID counter - TestCowImpl.ID_COUNTER.set(1); - - Thread.sleep(2000L); - } - - @Test - public - void rmiLocal() throws SecurityException, IOException, InterruptedException { - rmi(new Config() { - @Override - public - void apply(final Configuration configuration) { - configuration.localChannelName = EndPoint.LOCAL_CHANNEL; - } - }); - - // have to reset the object ID counter - TestCowImpl.ID_COUNTER.set(1); - - Thread.sleep(2000L); - } - - - public - void rmi(final Config config) throws SecurityException, IOException { - Configuration configuration = new Configuration(); - config.apply(configuration); - - configuration.serialization = Serialization.DEFAULT(); - register(configuration.serialization); - - // for Client -> Server RMI - configuration.serialization.registerRmi(TestCow.class, TestCowImpl.class); - - - final Server server = new Server(configuration); - server.setIdleTimeout(0); - - addEndPoint(server); - server.bind(false); - - final Listeners listeners = server.listeners(); - listeners.add(new Listener.OnMessageReceived() { - @Override - public - void received(final Connection connection, MessageWithTestCow m) { - System.err.println("Received finish signal for test for: Client -> Server"); - - TestCow object = m.getTestCow(); - final int id = object.id(); - assertEquals(1, id); - System.err.println("Finished test for: Client -> Server"); - - - System.err.println("Starting test for: Server -> Client"); - - // normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug - // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestCow.class, new RemoteObjectCallback() { - @Override - public - void created(final TestCow remoteObject) { - // MUST run on a separate thread because remote object method invocations are blocking - new Thread() { - @Override - public - void run() { - System.err.println("Running test for: Server -> Client"); - runTests(connection, remoteObject, 2); - System.err.println("Done with test for: Server -> Client"); - } - }.start(); - } - }); - } - }); - - - // ---- - configuration = new Configuration(); - config.apply(configuration); - - configuration.serialization = Serialization.DEFAULT(); - register(configuration.serialization); - - // this is for testing the "screwed up registrations logic". It should screwup for both network AND local-JVM connections - // configuration.serialization.register(ExtraClassTest1.class); - - // // for Server -> Client RMI - configuration.serialization.registerRmi(TestCow.class, TestCowImpl.class); - - - final Client client = new Client(configuration); - client.setIdleTimeout(0); - - addEndPoint(client); - - - client.listeners().add(new Listener.OnConnected() { - @Override - public - void connected(final Connection connection) { - System.err.println("Starting test for: Client -> Server"); - - // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestCow.class, new RemoteObjectCallback() { - @Override - public - void created(final TestCow remoteObject) { - // MUST run on a separate thread because remote object method invocations are blocking - new Thread() { - @Override - public - void run() { - System.err.println("Running test for: Client -> Server"); - runTests(connection, remoteObject, 1); - System.err.println("Done with test for: Client -> Server"); - } - }.start(); - } - }); - } - }); - - client.listeners() - .add(new Listener.OnMessageReceived() { - @Override - public - void received(Connection connection, MessageWithTestCow m) { - System.err.println("Received finish signal for test for: Client -> Server"); - - TestCow object = m.getTestCow(); - final int id = object.id(); - assertEquals(2, id); - System.err.println("Finished test for: Client -> Server"); - - stopEndPoints(2000); - } - }); - - client.connect(0); - - waitForThreads(); - } - - private static - class ExtraClassTest1 { - int foo = 0; - - public - ExtraClassTest1(final int foo) { - this.foo = foo; - } - } - - private static - class ExtraClassTest2 { - int foo = 0; - - public - ExtraClassTest2(final int foo) { - this.foo = foo; - } - } -} diff --git a/test-orig/dorkbox/network/rmi/TestCowImpl.java b/test-orig/dorkbox/network/rmi/TestCowImpl.java deleted file mode 100644 index e9911ad8..00000000 --- a/test-orig/dorkbox/network/rmi/TestCowImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2019 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 java.util.concurrent.atomic.AtomicInteger; - -/** - * - */ -public -class TestCowImpl extends TestCowBaseImpl implements TestCow { - // has to start at 1, because UDP method invocations ignore return values - static final AtomicInteger ID_COUNTER = new AtomicInteger(1); - - private int moos; - private final int id = ID_COUNTER.getAndIncrement(); - - public - TestCowImpl() { - } - - @Override - public - void moo() { - this.moos++; - System.out.println("Moo! " + this.moos); - } - - @Override - public - void moo(String value) { - this.moos += 2; - System.out.println("Moo! " + this.moos + ": " + value); - } - - @Override - public - void moo(String value, long delay) { - this.moos += 4; - System.out.println("Moo! " + this.moos + ": " + value + " (" + delay + ")"); - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public - int id() { - return id; - } - - @Override - public - float slow() { - System.out.println("Slowdown!!"); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return 123.0F; - } - - @Override - public - boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final TestCowImpl that = (TestCowImpl) o; - - return id == that.id; - - } - - @Override - public - int hashCode() { - return id; - } - - @Override - public - String toString() { - return "Tada! This is a remote object!"; - } -} diff --git a/test/dorkbox/network/AeronClient.kt b/test/dorkbox/network/AeronClient.kt index bef14e29..87047068 100644 --- a/test/dorkbox/network/AeronClient.kt +++ b/test/dorkbox/network/AeronClient.kt @@ -115,7 +115,8 @@ object AeronClient { } runBlocking { - client.connect("127.0.0.1") +// client.connect("127.0.0.1") // UDP connection via loopback + client.connect() // IPC connection } diff --git a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.kt b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.kt index 04bc1c8d..6d9499a6 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.kt +++ b/test/dorkbox/network/rmi/RmiSendObjectOverrideMethodTest.kt @@ -53,8 +53,9 @@ class RmiSendObjectOverrideMethodTest : BaseTest() { } /** - * In this test the server has two objects in an object space. The client - * uses the first remote object to get the second remote object. + * In this test the server has two objects in an object space. + * + * The client uses the first remote object to get the second remote object. * * * The MAJOR difference in this version, is that we use an interface to override the methods, so that we can have the RMI system pass @@ -117,35 +118,35 @@ class RmiSendObjectOverrideMethodTest : BaseTest() { client.onConnect { connection -> // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestObject::class.java, object : RemoteObjectCallback { - override fun created(remoteObject: TestObject) { - // MUST run on a separate thread because remote object method invocations are blocking - object : Thread() { - override fun run() { - remoteObject.setOther(43.21f) - - // Normal remote method call. - Assert.assertEquals(43.21f, remoteObject.other(), .0001f) - - // Make a remote method call that returns another remote proxy object. - // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created. - // here we have a proxy to both of them. - val otherObject = remoteObject.getOtherObject() - - // Normal remote method call on the second object. - otherObject.setValue(12.34f) - val value = otherObject.value() - Assert.assertEquals(12.34f, value, .0001f) - - // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because - // that is where that object actually exists. - runBlocking { - connection.send(otherObject) - } - } - }.start() - } - }) +// connection.create(TestObject::class.java, object : RemoteObjectCallback { +// override suspend fun created(remoteObject: TestObject) { +// // MUST run on a separate thread because remote object method invocations are blocking +// object : Thread() { +// override fun run() { +// remoteObject.setOther(43.21f) +// +// // Normal remote method call. +// Assert.assertEquals(43.21f, remoteObject.other(), .0001f) +// +// // Make a remote method call that returns another remote proxy object. +// // the "test" object exists in the REMOTE side, as does the "OtherObject" that is created. +// // here we have a proxy to both of them. +// val otherObject = remoteObject.getOtherObject() +// +// // Normal remote method call on the second object. +// otherObject.setValue(12.34f) +// val value = otherObject.value() +// Assert.assertEquals(12.34f, value, .0001f) +// +// // When a proxy object is sent, the other side receives its ACTUAL object (not a proxy of it), because +// // that is where that object actually exists. +// runBlocking { +// connection.send(otherObject) +// } +// } +// }.start() +// } +// }) } runBlocking { diff --git a/test/dorkbox/network/rmi/RmiSendObjectTest.kt b/test/dorkbox/network/rmi/RmiSendObjectTest.kt index bcb5ce74..b32d59f7 100644 --- a/test/dorkbox/network/rmi/RmiSendObjectTest.kt +++ b/test/dorkbox/network/rmi/RmiSendObjectTest.kt @@ -104,32 +104,32 @@ class RmiSendObjectTest : BaseTest() { val client = Client(configuration) addEndPoint(client) client.onConnect { connection -> - connection.createRemoteObject(TestObject::class.java, object : RemoteObjectCallback { - override fun created(remoteObject: TestObject) { - // MUST run on a separate thread because remote object method invocations are blocking - object : Thread() { - override fun run() { - remoteObject.setOther(43.21f) - - // Normal remote method call. - Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f) - - // Make a remote method call that returns another remote proxy object. - val otherObject = remoteObject.getOtherObject() - - // Normal remote method call on the second object. - otherObject.setValue(12.34f) - val value = otherObject.value() - Assert.assertEquals(12.34f, value, 0.0001f) - - // When a remote proxy object is sent, the other side receives its actual remote object. - runBlocking { - connection.send(otherObject) - } - } - }.start() - } - }) +// connection.create(TestObject::class.java, object : RemoteObjectCallback { +// override suspend fun created(remoteObject: TestObject) { +// // MUST run on a separate thread because remote object method invocations are blocking +// object : Thread() { +// override fun run() { +// remoteObject.setOther(43.21f) +// +// // Normal remote method call. +// Assert.assertEquals(43.21f, remoteObject.other(), 0.0001f) +// +// // Make a remote method call that returns another remote proxy object. +// val otherObject = remoteObject.getOtherObject() +// +// // Normal remote method call on the second object. +// otherObject.setValue(12.34f) +// val value = otherObject.value() +// Assert.assertEquals(12.34f, value, 0.0001f) +// +// // When a remote proxy object is sent, the other side receives its actual remote object. +// runBlocking { +// connection.send(otherObject) +// } +// } +// }.start() +// } +// }) } runBlocking { diff --git a/test/dorkbox/network/rmi/RmiTest.kt b/test/dorkbox/network/rmi/RmiTest.kt index 754cf519..adf020b2 100644 --- a/test/dorkbox/network/rmi/RmiTest.kt +++ b/test/dorkbox/network/rmi/RmiTest.kt @@ -34,10 +34,7 @@ */ package dorkbox.network.rmi -import dorkbox.network.BaseTest -import dorkbox.network.Client -import dorkbox.network.Configuration -import dorkbox.network.Server +import dorkbox.network.* import dorkbox.network.connection.Connection import dorkbox.network.rmi.classes.MessageWithTestCow import dorkbox.network.rmi.classes.TestCow @@ -85,10 +82,22 @@ class RmiTest : BaseTest() { Assert.assertTrue(caught) + // can ONLY wait for responses if we are ASYNC! + caught = false + try { + remoteObject.waitForLastResponse() + } catch (ex: IllegalStateException) { + caught = true + } + Assert.assertTrue(caught) + + // Non-blocking call tests // Non-blocking call tests // Non-blocking call tests - remoteObject.setAsync(true) + System.err.println("I'm currently async: ${remoteObject.async}") + + remoteObject.async = true // calls that ignore the return value @@ -99,37 +108,43 @@ class RmiTest : BaseTest() { // exceptions are still dealt with properly test.moo("Baa") - test.id() + caught = false try { test.throwException() - } catch (ex: UnsupportedOperationException) { + } catch (ex: IllegalStateException) { caught = true } - Assert.assertTrue(caught) - remoteObject.setAsync(true) + // exceptions are not caught when async = true! + Assert.assertFalse(caught) - // wait for the response to id() EVEN THOUGH IT IS ASYNC? - Assert.assertEquals(remoteObjectID, remoteObject.waitForLastResponse()) - Assert.assertEquals(0, test.id().toLong()) + // now enable us to wait for responses + // can ONLY wait for responses if we are ASYNC + enabled waiting!! + remoteObject.enableWaitingForResponse(true) - val responseID = remoteObject.lastResponseID + + test.id() // wait for the response to id() - Assert.assertEquals(remoteObjectID, remoteObject.waitForResponse(responseID)) + Assert.assertEquals(remoteObjectID, remoteObject.waitForLastResponse()) + + + // wait for the response to id() + Assert.assertEquals(0, test.id().toLong()) + val responseId = remoteObject.lastResponseId + Assert.assertEquals(remoteObjectID, remoteObject.waitForResponse(responseId)) + // Non-blocking call that errors out -// remoteObject.setTransmitReturnValue(false) test.throwException() Assert.assertEquals(remoteObject.waitForLastResponse()?.javaClass, UnsupportedOperationException::class.java) // Call will time out if non-blocking isn't working properly -// remoteObject.setTransmitExceptions(false) - test.moo("Mooooooooo", 3000) + test.moo("Mooooooooo", 4000) + // should wait for a small time -// remoteObject.setTransmitReturnValue(true) - remoteObject.setAsync(false) + remoteObject.async = false remoteObject.responseTimeout = 6000 println("You should see this 2 seconds before") val slow = test.slow() @@ -155,7 +170,17 @@ class RmiTest : BaseTest() { @Test @Throws(SecurityException::class, IOException::class, InterruptedException::class) - fun rmiNetwork() { + fun rmiNetworkGlobal() { + rmiGlobal() + + // have to reset the object ID counter + TestCowImpl.ID_COUNTER.set(1) + Thread.sleep(2000L) + } + + @Test + @Throws(SecurityException::class, IOException::class, InterruptedException::class) + fun rmiNetworkConnection() { rmi() // have to reset the object ID counter @@ -166,9 +191,10 @@ class RmiTest : BaseTest() { @Test @Throws(SecurityException::class, IOException::class, InterruptedException::class) fun rmiIPC() { - TODO("DO IPC STUFF!") rmi { configuration -> -// configuration.localChannelName = EndPoint.LOCAL_CHANNEL + if (configuration is ServerConfiguration) { + configuration.listenIpAddress = LOOPBACK + } } // have to reset the object ID counter @@ -200,21 +226,11 @@ class RmiTest : BaseTest() { System.err.println("Starting test for: Server -> Client") // normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug - // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback { - override fun created(remoteObject: TestCow) { - // MUST run on a separate thread because remote object method invocations are blocking - object : Thread() { - override fun run() { - System.err.println("Running test for: Server -> Client") - runBlocking { - runTests(connection, remoteObject, 2) - System.err.println("Done with test for: Server -> Client") - } - } - }.start() - } - }) + connection.createObject { remoteObject -> + System.err.println("Running test for: Server -> Client") + runTests(connection, remoteObject, 2) + System.err.println("Done with test for: Server -> Client") + } } } @@ -223,10 +239,7 @@ class RmiTest : BaseTest() { config(configuration) register(configuration.serialization) - // this is for testing the "screwed up registrations logic". It should screwup for both network AND local-JVM connections - // configuration.serialization.register(ExtraClassTest1.class); - - // // for Server -> Client RMI + // for Server -> Client RMI configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) val client = Client(configuration) addEndPoint(client) @@ -234,24 +247,14 @@ class RmiTest : BaseTest() { client.onConnect { connection -> System.err.println("Starting test for: Client -> Server") - // if this is called in the dispatch thread, it will block network comms while waiting for a response and it won't work... - connection.createRemoteObject(TestCow::class.java, object : RemoteObjectCallback { - override fun created(remoteObject: TestCow) { - // MUST run on a separate thread because remote object method invocations are blocking - object : Thread() { - override fun run() { - System.err.println("Running test for: Client -> Server") - runBlocking { - runTests(connection, remoteObject, 1) - } - System.err.println("Done with test for: Client -> Server") - } - }.start() - } - }) + connection.createObject { remoteObject -> + System.err.println("Running test for: Client -> Server") + runTests(connection, remoteObject, 1) + System.err.println("Done with test for: Client -> Server") + } } - client.onMessage { connection, m -> + client.onMessage { _, m -> System.err.println("Received finish signal for test for: Client -> Server") val `object` = m.testCow val id = `object`.id() @@ -268,19 +271,73 @@ class RmiTest : BaseTest() { waitForThreads() } - private class ExtraClassTest1(foo: Int) { - var foo = 0 + @Throws(SecurityException::class, IOException::class) + fun rmiGlobal(config: (Configuration) -> Unit = {}) { + run { + val configuration = serverConfig() + config(configuration) + register(configuration.serialization) - init { - this.foo = foo + // for Client -> Server RMI + configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) + + val server = Server(configuration) + addEndPoint(server) + + server.bind(false) + + server.onMessage { connection, m -> + System.err.println("Received finish signal for test for: Client -> Server") + + val `object` = m.testCow + val id = `object`.id() + + Assert.assertEquals(1, id.toLong()) + + System.err.println("Finished test for: Client -> Server") + + // normally this is in the 'connected', but we do it here, so that it's more linear and easier to debug + connection.createObject { remoteObject -> + System.err.println("Running test for: Server -> Client") + runTests(connection, remoteObject, 2) + System.err.println("Done with test for: Server -> Client") + } + } } - } - private class ExtraClassTest2(foo: Int) { - var foo = 0 + run { + val configuration = clientConfig() + config(configuration) + register(configuration.serialization) - init { - this.foo = foo + // for Server -> Client RMI + configuration.serialization.registerRmi(TestCow::class.java, TestCowImpl::class.java) + val client = Client(configuration) + addEndPoint(client) + + client.onMessage { _, m -> + System.err.println("Received finish signal for test for: Client -> Server") + val `object` = m.testCow + val id = `object`.id() + Assert.assertEquals(2, id.toLong()) + System.err.println("Finished test for: Client -> Server") + stopEndPoints(2000) + } + + runBlocking { + client.connect(LOOPBACK) + + System.err.println("Starting test for: Client -> Server") + + // this creates a GLOBAL object on the server (instead of a connection specific object) + client.createObject { remoteObject -> + System.err.println("Running test for: Client -> Server") + runTests(client.getConnection(), remoteObject, 1) + System.err.println("Done with test for: Client -> Server") + } + } } + + waitForThreads() } } diff --git a/test/dorkbox/network/rmi/classes/MessageWithTestCow.kt b/test/dorkbox/network/rmi/classes/MessageWithTestCow.kt index dddc938b..9acd33bd 100644 --- a/test/dorkbox/network/rmi/classes/MessageWithTestCow.kt +++ b/test/dorkbox/network/rmi/classes/MessageWithTestCow.kt @@ -14,9 +14,6 @@ */ package dorkbox.network.rmi.classes -/** - * - */ class MessageWithTestCow(val testCow: TestCow) { var number = 0 var text: String? = null diff --git a/test/dorkbox/network/rmi/classes/TestCowBaseImpl.kt b/test/dorkbox/network/rmi/classes/TestCowBaseImpl.kt index 91138c64..4e30e6bd 100644 --- a/test/dorkbox/network/rmi/classes/TestCowBaseImpl.kt +++ b/test/dorkbox/network/rmi/classes/TestCowBaseImpl.kt @@ -14,11 +14,9 @@ */ package dorkbox.network.rmi.classes -/** - * - */ open class TestCowBaseImpl : TestCowBase { override fun throwException() { + System.err.println("The following exception is EXPECTED, but should only be on one log!") throw UnsupportedOperationException("Why would I do that?") }