Compare commits

..

No commits in common. "271cdd18e3a65170cd4e146d32b9b8ea9ea73140" and "7bebbf7bd7c96a04f4e6295b5f2a1cf103d34e40" have entirely different histories.

6 changed files with 15 additions and 103 deletions

View File

@ -46,7 +46,6 @@ class KiloClientConnection<S>(
suspend fun run(onConnectedStateChanged: ((Boolean) -> Unit)? = null) { suspend fun run(onConnectedStateChanged: ((Boolean) -> Unit)? = null) {
coroutineScope { coroutineScope {
var job: Job? = null var job: Job? = null
var connectedScope: KiloScope<S>? = null
try { try {
// in parallel: keys and connection // in parallel: keys and connection
val deferredKeyPair = async { SafeKeyExchange() } val deferredKeyPair = async { SafeKeyExchange() }
@ -91,8 +90,7 @@ class KiloClientConnection<S>(
kiloRemoteInterface.complete( kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface) KiloRemoteInterface(deferredParams, clientInterface)
) )
connectedScope = params.scope clientInterface.onConnectHandlers.invokeAll(params.scope)
clientInterface.onConnectHandlers.invokeAll(connectedScope)
onConnectedStateChanged?.invoke(true) onConnectedStateChanged?.invoke(true)
job.join() job.join()
@ -101,7 +99,6 @@ class KiloClientConnection<S>(
} catch (x: RemoteInterface.ClosedException) { } catch (x: RemoteInterface.ClosedException) {
debug { "connection closed/refused by remote" } debug { "connection closed/refused by remote" }
} finally { } finally {
connectedScope?.let { clientInterface.onDisconnectHandlers.invokeAll(it) }
onConnectedStateChanged?.invoke(false) onConnectedStateChanged?.invoke(false)
job?.cancel() job?.cancel()
device.apply { runCatching { close() } } device.apply { runCatching { close() } }

View File

@ -19,17 +19,16 @@ typealias KiloHandler<S> = KiloScope<S>.()->Unit
* *
* - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and * - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and
* `IllegalStateException` * `IllegalStateException`
* - It provides [onConnected] and [onDisconnected] handlers * - It provides [onConnected] handler
* *
* See [KiloServer] for usage sample. * See [KiloServer] for usage sample.
*/ */
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() { open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
internal val onConnectHandlers = mutableListOf<KiloHandler<S>>() internal val onConnectHandlers = mutableListOf<KiloHandler<S>>()
internal val onDisconnectHandlers = mutableListOf<KiloHandler<S>>()
/** /**
* Registers handler [f] for [onConnected] event, to the head or the end of a handler list. * Registers handler [f] for [onConnected] event, to the head or the end of handler list.
* *
* @param addFirst if true, [f] will be added to the beginning of the list of handlers * @param addFirst if true, [f] will be added to the beginning of the list of handlers
*/ */
@ -37,18 +36,6 @@ open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
if( addFirst ) onConnectHandlers.add(0, f) else onConnectHandlers += f 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<S>.()->Unit) {
if( addFirst ) onDisconnectHandlers.add(0, f) else onDisconnectHandlers += f
}
init { init {
registerError { RemoteInterface.UnknownCommand(it) } registerError { RemoteInterface.UnknownCommand(it) }
registerError { RemoteInterface.InternalError(it) } registerError { RemoteInterface.InternalError(it) }
@ -60,3 +47,4 @@ open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
registerError { IllegalArgumentException(it) } registerError { IllegalArgumentException(it) }
} }
} }

View File

@ -53,7 +53,6 @@ class KiloServerConnection<S>(
suspend fun run() { suspend fun run() {
val deferredParams = CompletableDeferred<KiloParams<S>>() val deferredParams = CompletableDeferred<KiloParams<S>>()
val deferredTransport = CompletableDeferred<Transport<*>>() val deferredTransport = CompletableDeferred<Transport<*>>()
var connectedScope: KiloScope<S>? = null
val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply { val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
var params: KiloParams<S>? = null var params: KiloParams<S>? = null
@ -86,8 +85,7 @@ class KiloServerConnection<S>(
kiloRemoteInterface.complete( kiloRemoteInterface.complete(
KiloRemoteInterface(deferredParams, clientInterface) KiloRemoteInterface(deferredParams, clientInterface)
) )
connectedScope = p.scope clientInterface.onConnectHandlers.invokeAll(p.scope)
clientInterface.onConnectHandlers.invokeAll(connectedScope)
} }
} }
@ -95,12 +93,8 @@ class KiloServerConnection<S>(
deferredTransport.complete(transport) deferredTransport.complete(transport)
kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface)) kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
debug { "starting the transport"} debug { "starting the transport"}
try {
transport.run() transport.run()
debug { "server transport finished" } debug { "server transport finished" }
} finally {
connectedScope?.let { clientInterface.onDisconnectHandlers.invokeAll(it) }
}
} }
companion object { companion object {

View File

@ -53,18 +53,16 @@ fun <S> websocketClient(
/** /**
* Create kilopaarsec transport over websocket (ws or wss). * Create kilopaarsec transport over websocket (ws or wss).
* @param path websocket path (must start with ws:// or wss:// and contain a path part) * @param path websocket path (must start with ws:// or wss:// and contain a path part)
* @param client optional caller-owned client. When omitted, a client with the * @client use default [HttpClient], it installs [WebSockets] plugin
* [WebSockets] plugin is created for this device and closed with it.
*/ */
fun websocketTransportDevice( fun websocketTransportDevice(
path: String, path: String,
useTextFrames: Boolean = false, useTextFrames: Boolean = false,
client: HttpClient? = null, client: HttpClient = HttpClient {
install(WebSockets)
},
): Transport.Device { ): Transport.Device {
val ownsClient = client == null
val actualClient = client ?: HttpClient { install(WebSockets) }
val log = LogTag("WSTD") val log = LogTag("WSTD")
var u = Url(path) var u = Url(path)
log.debug { "Creating websocket transport device at $u" } log.debug { "Creating websocket transport device at $u" }
@ -84,7 +82,7 @@ fun websocketTransportDevice(
globalLaunch { globalLaunch {
val log = LogTag("KC:${counter.incrementAndGet()}") val log = LogTag("KC:${counter.incrementAndGet()}")
try { try {
actualClient.webSocket({ client.webSocket({
url.protocol = u.protocol url.protocol = u.protocol
url.host = u.host url.host = u.host
url.port = u.port url.port = u.port
@ -150,8 +148,6 @@ fun websocketTransportDevice(
else log.warning { "unexpected IO error $x" } else log.warning { "unexpected IO error $x" }
runCatching { output.close() } runCatching { output.close() }
runCatching { input.close() } runCatching { input.close() }
} finally {
if (ownsClient) actualClient.close()
} }
log.info { "closing connection" } log.info { "closing connection" }
} }
@ -166,3 +162,4 @@ fun websocketTransportDevice(
}) })
return device return device
} }

View File

@ -170,41 +170,6 @@ class TransportTest {
d2.close() d2.close()
} }
@Test
fun testDisconnectedHandlers() = runTest {
initCrypto()
val cmdPing by command<String, String>()
val (d1, d2) = createTestDevice()
val serverDisconnected = CompletableDeferred<String>()
val clientDisconnected = CompletableDeferred<String>()
val serverInterface = KiloInterface<String>().apply {
onDisconnected {
serverDisconnected.complete(session)
}
on(cmdPing) {
"pong! [$it]"
}
}
launch { KiloServerConnection(serverInterface, d1, "server session").run() }
val clientInterface = KiloInterface<String>().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) class TestException(text: String) : Exception(text)
@Test @Test

View File

@ -1,29 +0,0 @@
package net.sergeych.kiloparsec.adapter
import java.nio.file.Files
import java.nio.file.Path
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertTrue
class WebsocketClientResourceTest {
@Test
fun failedReconnectsDoNotLeakFileDescriptors() = runBlocking {
val descriptors = Path.of("/proc/self/fd")
if (!Files.isDirectory(descriptors)) return@runBlocking
val client = websocketClient<Unit>("ws://127.0.0.1:1/kp")
try {
delay(2_500)
val before = descriptorCount(descriptors)
delay(7_000)
val after = descriptorCount(descriptors)
assertTrue(after <= before + 3, "file descriptors grew from $before to $after")
} finally {
client.close()
}
}
private fun descriptorCount(path: Path): Long = Files.list(path).use { it.count() }
}