diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloClientConnection.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloClientConnection.kt index 60df23a..816e881 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloClientConnection.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloClientConnection.kt @@ -46,6 +46,7 @@ class KiloClientConnection( suspend fun run(onConnectedStateChanged: ((Boolean) -> Unit)? = null) { coroutineScope { var job: Job? = null + var connectedScope: KiloScope? = null try { // in parallel: keys and connection val deferredKeyPair = async { SafeKeyExchange() } @@ -90,7 +91,8 @@ class KiloClientConnection( kiloRemoteInterface.complete( KiloRemoteInterface(deferredParams, clientInterface) ) - clientInterface.onConnectHandlers.invokeAll(params.scope) + connectedScope = params.scope + clientInterface.onConnectHandlers.invokeAll(connectedScope) onConnectedStateChanged?.invoke(true) job.join() @@ -99,6 +101,7 @@ class KiloClientConnection( } catch (x: RemoteInterface.ClosedException) { debug { "connection closed/refused by remote" } } finally { + connectedScope?.let { clientInterface.onDisconnectHandlers.invokeAll(it) } onConnectedStateChanged?.invoke(false) job?.cancel() device.apply { runCatching { close() } } @@ -116,4 +119,4 @@ class KiloClientConnection( } internal fun Collection>.invokeAll(scope: KiloScope) = - forEach { runCatching { scope.it() } } \ No newline at end of file + forEach { runCatching { scope.it() } } diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt index 6f4352a..e07da6c 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloInterface.kt @@ -19,16 +19,17 @@ typealias KiloHandler = KiloScope.()->Unit * * - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and * `IllegalStateException` - * - It provides [onConnected] handler + * - It provides [onConnected] and [onDisconnected] handlers * * See [KiloServer] for usage sample. */ open class KiloInterface : LocalInterface>() { internal val onConnectHandlers = mutableListOf>() + internal val onDisconnectHandlers = mutableListOf>() /** - * Registers handler [f] for [onConnected] event, to the head or the end of handler list. + * Registers handler [f] for [onConnected] event, to the head or the end of a handler list. * * @param addFirst if true, [f] will be added to the beginning of the list of handlers */ @@ -36,6 +37,18 @@ open class KiloInterface : LocalInterface>() { if( addFirst ) onConnectHandlers.add(0, f) else onConnectHandlers += f } + /** + * Registers handler [f] for [onDisconnected] event, to the head or the end of a handler list. + * + * It is called with the same connection [KiloScope] as [onConnected], after an established + * connection is closed. + * + * @param addFirst if true, [f] will be added to the beginning of the list of handlers + */ + fun onDisconnected(addFirst: Boolean = false, f: KiloScope.()->Unit) { + if( addFirst ) onDisconnectHandlers.add(0, f) else onDisconnectHandlers += f + } + init { registerError { RemoteInterface.UnknownCommand(it) } registerError { RemoteInterface.InternalError(it) } @@ -47,4 +60,3 @@ open class KiloInterface : LocalInterface>() { registerError { IllegalArgumentException(it) } } } - diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServerConnection.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServerConnection.kt index f1950b6..1506157 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServerConnection.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServerConnection.kt @@ -53,6 +53,7 @@ class KiloServerConnection( suspend fun run() { val deferredParams = CompletableDeferred>() val deferredTransport = CompletableDeferred>() + var connectedScope: KiloScope? = null val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply { var params: KiloParams? = null @@ -85,7 +86,8 @@ class KiloServerConnection( kiloRemoteInterface.complete( KiloRemoteInterface(deferredParams, clientInterface) ) - clientInterface.onConnectHandlers.invokeAll(p.scope) + connectedScope = p.scope + clientInterface.onConnectHandlers.invokeAll(connectedScope) } } @@ -93,8 +95,12 @@ class KiloServerConnection( deferredTransport.complete(transport) kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface)) debug { "starting the transport"} - transport.run() - debug { "server transport finished" } + try { + transport.run() + debug { "server transport finished" } + } finally { + connectedScope?.let { clientInterface.onDisconnectHandlers.invokeAll(it) } + } } companion object { @@ -108,4 +114,4 @@ class KiloServerConnection( override suspend fun push(cmd: Command, args: A) { kiloRemoteInterface.await().push(cmd, args) } -} \ No newline at end of file +} diff --git a/src/commonTest/kotlin/TransportTest.kt b/src/commonTest/kotlin/TransportTest.kt index 63b381c..8145d9a 100644 --- a/src/commonTest/kotlin/TransportTest.kt +++ b/src/commonTest/kotlin/TransportTest.kt @@ -170,6 +170,41 @@ class TransportTest { d2.close() } + @Test + fun testDisconnectedHandlers() = runTest { + initCrypto() + + val cmdPing by command() + val (d1, d2) = createTestDevice() + val serverDisconnected = CompletableDeferred() + val clientDisconnected = CompletableDeferred() + + val serverInterface = KiloInterface().apply { + onDisconnected { + serverDisconnected.complete(session) + } + on(cmdPing) { + "pong! [$it]" + } + } + launch { KiloServerConnection(serverInterface, d1, "server session").run() } + + val clientInterface = KiloInterface().apply { + onDisconnected { + clientDisconnected.complete(session) + } + } + val client = KiloClientConnection(clientInterface, d2, "client session") + launch { client.run() } + + assertEquals("pong! [hello]", client.call(cmdPing, "hello")) + d1.close() + d2.close() + + assertEquals("server session", withTimeout(1000) { serverDisconnected.await() }) + assertEquals("client session", withTimeout(1000) { clientDisconnected.await() }) + } + class TestException(text: String) : Exception(text) @Test