From 0d3a8ae95ce11956ad3d17c500d72599e105c1ce Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 17 Jun 2024 13:01:09 +0700 Subject: [PATCH] separate BreakConnectionException and more correct processing for ClosedException. --- .../net/sergeych/kiloparsec/KiloInterface.kt | 3 +- .../sergeych/kiloparsec/KiloL0Interface.kt | 2 +- .../net/sergeych/kiloparsec/KiloServer.kt | 4 +- .../net/sergeych/kiloparsec/LocalInterface.kt | 30 ++++++++++++- .../sergeych/kiloparsec/RemoteInterface.kt | 7 ++- .../net/sergeych/kiloparsec/Transport.kt | 10 +++-- src/commonTest/kotlin/TransportTest.kt | 44 ++++++++++++++++--- 7 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt index 1e915ff..ac3d8ed 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt @@ -14,8 +14,9 @@ class KiloInterface : LocalInterface>() { init { registerError { RemoteInterface.UnknownCommand() } + registerError { RemoteInterface.InternalError(it) } registerError { RemoteInterface.ClosedException(it) } - registerError { RemoteInterface.SecurityException(it) } +// registerError { RemoteInterface.SecurityException(it) } registerError { RemoteInterface.InvalidDataException(it) } registerError { RemoteInterface.RemoteException(it) } registerError { IllegalStateException() } diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloL0Interface.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloL0Interface.kt index 54274a9..f95de90 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloL0Interface.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloL0Interface.kt @@ -33,7 +33,7 @@ internal class KiloL0Interface( 0u, clientInterface.execute(params.scope, call.name, call.serializedArgs) ) - } catch(t: RemoteInterface.ClosedException) { + } catch(t: BreakConnectionException) { throw t } catch (t: Throwable) { diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt index c9e10ee..4fa4e95 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt @@ -32,8 +32,8 @@ class KiloServer( } catch(_: CancellationException) { } - catch(_: RemoteInterface.ClosedException) { - info { "Closed exception caught, closing" } + catch(cce: LocalInterface.BreakConnectionException) { + info { "Closed exception caught, closing (${cce.flushSendQueue}" } } catch (t: Throwable) { exception { "unexpected while creating kiloclient" to t } diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/LocalInterface.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/LocalInterface.kt index 3ca9ddf..fe16b8e 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/LocalInterface.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/LocalInterface.kt @@ -2,6 +2,7 @@ package net.sergeych.kiloparsec import net.sergeych.mp_logger.LogTag import net.sergeych.mp_logger.Loggable +import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.info import net.sergeych.tools.AtomicCounter import net.sergeych.utools.firstNonNull @@ -15,6 +16,26 @@ open class LocalInterface : Loggable by LogTag("LocalInterface${idCounter.inc private val commands = mutableMapOf>() + /** + * Instruct the transport to immediately break the connection. + * This exception is not passed to the remote end, instead, transport device breaks + * connection to remote when receiving it. + * + * Remote interface will throw [RemoteInterface.ClosedException] as the break will be detected. As reaction time + * it depends on the transport in use, we recommend sending some registered exception first if you need + * to pass important data, or implement special commands on both sides. + * + * __Important note:__ _it is not allowed to throw [RemoteInterface.ClosedException] directly!_ + * This exception is processed internally and can't be sent over the network. + */ + open class BreakConnectionException( + text: String = "break connection request", + val flushSendQueue: Boolean = true, + ) : RuntimeException(text) { + override val message: String? + get() = super.message + " (flush=$flushSendQueue)" + } + /** * New session creator. Rarely needed directlym it can be used for delegation * of local interfaces. @@ -77,8 +98,13 @@ open class LocalInterface : Loggable by LogTag("LocalInterface${idCounter.inc errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) } fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error = - getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) } - ?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}") + if (t is RemoteInterface.ClosedException) { + exception { "Illegal attempt to send ClosedException" to t } + encodeError(forId, RemoteInterface.InternalError("TCE")) + } + else + getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) } + ?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}") open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? = errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) } diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/RemoteInterface.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/RemoteInterface.kt index 1589098..5bb2b2f 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/RemoteInterface.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/RemoteInterface.kt @@ -15,11 +15,12 @@ interface RemoteInterface { /** * Is thrown when the channel is closed, in an attempt to execute a command, also to all pending - * calls (see [call]). + * calls (see [call]). Client code should never throw it. If command handler needs to break connection + * it should throw [LocalInterface.BreakConnectionException] */ open class ClosedException(t: String = "connection is closed") : Exception(t) - open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t) + open class SecurityException(t: String = "invalid remote id and signature") : LocalInterface.BreakConnectionException(t) open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg) @@ -41,6 +42,8 @@ interface RemoteInterface { */ class UnknownCommand : RemoteException("UnknownCommand") + open class InternalError(code: String="0"): RemoteException("Internal error: $code") + suspend fun call(cmd: Command): R = call(cmd, Unit) /** diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/Transport.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/Transport.kt index e83fafd..db15959 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/Transport.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/Transport.kt @@ -180,11 +180,11 @@ class Transport( localInterface.execute(commandContext, b.name, b.packedArgs) ) ) - } catch (x: RemoteInterface.ClosedException) { + } catch (x: LocalInterface.BreakConnectionException) { // handler forced close - warning { "handler requested closing of the connection"} + warning { "handler requested closing of the connection (${x.flushSendQueue}"} isClosed = true - throw x + device.close() } catch (x: RemoteInterface.RemoteException) { send(Block.Error(b.id, x.code, x.text, x.extra)) } catch (t: Throwable) { @@ -203,6 +203,10 @@ class Transport( info { "closed receive channel"} isClosed = true } + catch(cce: LocalInterface.BreakConnectionException) { + info { "closing connection by local request ($cce)"} + device.close() + } catch (_: CancellationException) { info { "loop is cancelled with CancellationException" } isClosed = true diff --git a/src/commonTest/kotlin/TransportTest.kt b/src/commonTest/kotlin/TransportTest.kt index 18918a7..5359c4b 100644 --- a/src/commonTest/kotlin/TransportTest.kt +++ b/src/commonTest/kotlin/TransportTest.kt @@ -7,7 +7,10 @@ import net.sergeych.crypto2.SigningKey import net.sergeych.crypto2.initCrypto import net.sergeych.kiloparsec.* import net.sergeych.mp_logger.Log -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.fail private var dcnt = 0 fun createTestDevice(): Pair { @@ -154,6 +157,8 @@ class TransportTest { d2.close() } + class TestException(text: String) : Exception(text) + @Test fun testClient() = runTest { initCrypto() @@ -173,6 +178,10 @@ class TransportTest { val serverId = SigningKey.pair() val clientId = SigningKey.pair() + val cmdException by command() + val cmdRemoteExceptionTest by command() + val cmdBreak by command() + val serverInterface = KiloInterface().apply { on(cmdPing) { "pong! [$it]" @@ -189,15 +198,26 @@ class TransportTest { on(cmdChainCallServer2) { remote.call(cmdChainCallClient2, "$it-s2") } - registerError { IllegalStateException() } - registerError { IllegalArgumentException(it) } + on(cmdException) { throw TestException("te1") } + on(cmdRemoteExceptionTest) { + try { + remote.call(cmdException) + "error!" + } catch (e: TestException) { + "ok: ${e.message}" + } + } + on(cmdBreak) { throw LocalInterface.BreakConnectionException() } + registerError { TestException(it) } } - val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.secretKey + val kiloServerConnection = KiloServerConnection( + serverInterface, d1, "server session", serverId.secretKey ) launch { kiloServerConnection.run() } var cnt = 0 val client = KiloClient { + addErrors(serverInterface) session { "client session!" } secretIdKey = clientId.secretKey local { @@ -208,11 +228,13 @@ class TransportTest { "client pong: $it" } on(cmdChainCallClient1) { - remote.call(cmdChainCallServer2,"$it-c1") + remote.call(cmdChainCallServer2, "$it-c1") } on(cmdChainCallClient2) { "$it-c2" } + on(cmdException) { throw TestException("te-local") } } connect { + println("Called connect: $cnt") if (cnt++ > 0) { cancel() fail("connect called once again") @@ -226,6 +248,18 @@ class TransportTest { assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar")) assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**")) + + assertThrows { client.call(cmdException) } + assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest)) + + assertThrows { + client.call(cmdBreak) + } + + // Note that current transport test is too simple, + // therefore, we can't test reconnecting, also we need server and client instances + // not connections, so that's all + d1.close() d2.close() client.close()