diff --git a/.gitignore b/.gitignore index e4ba2df..d2861fa 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,7 @@ out/ .project .settings .springBeans -.sts4-cache -bin/ +.sts4-caches !**/src/main/**/bin/ !**/src/test/**/bin/ diff --git a/README.md b/README.md index b1fbfde..cef920e 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,11 @@ val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0. ~~~ +## See also: + +- [Source documentation](https://code.sergeych.net/docs/kiloparsec/) +- [Project's WIKI](https://gitea.sergeych.net/sergeych/kiloparsec/wiki) + # Details It is not compatible with parsec family and no more based on an Universa crypto library. To better fit diff --git a/build.gradle.kts b/build.gradle.kts index 283612a..c040344 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { kotlin("multiplatform") version "2.0.0" id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" `maven-publish` + id("org.jetbrains.dokka") version "1.9.20" } group = "net.sergeych" @@ -135,3 +136,13 @@ kotlin { } } } + +tasks.dokkaHtml.configure { + outputDirectory.set(buildDir.resolve("dokka")) + dokkaSourceSets { + configureEach { +// includes.from("docs/bipack.md") + } + } +} + diff --git a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt index f889bb4..8f4ebda 100644 --- a/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt +++ b/src/commonMain/kotlin/net/sergeych/kiloparsec/KiloServer.kt @@ -13,13 +13,97 @@ import net.sergeych.mp_tools.globalLaunch import net.sergeych.tools.AtomicCounter private val instances = AtomicCounter() -@Suppress("unused") + +/** + * The Kiloparsec server. + * Server accepts incoming connections and serves them using the same [clientInterface]. + * + * ## Incoming connections + * + * Server collecting incoming connections provided by [connections] `Flow`. For each incoming connection + * the Kiloparsec handshake is performed, then the session object is created, see below, and connection is + * served with [clientInterface] until closed. + * + * ## Session param [S] + * + * After the successful handshake server creates new session for each connection calling the [sessionBuilder]. + * Then it creates a [KiloScope] so [KiloScope.session] holds this connection-specific instance. Then + * [KiloInterface.onConnected] is called with this scope. Session can be used to hold connection state. Session + * objects are not persistent, but could be initialized in [KiloInterface.onConnected] where the remote side + * [KiloScope.remoteIdentity] is already verified and set. + * + * ## Usage: + * + * Create a shared library between you server and clients, to specify the interface (otherwise you can + * share sources). + * + * Suppose we have session with a state: + * ```kotlin + * data class Session( + * var data: String, + * ) + *``` + * + * And some commands to access and change it, in the shared library too: + * + * ```kotlin + * val cmdSave by command() + * val cmdLoad by command() + * val cmdDrop by command() + * val cmdException by command() + * ``` + * Then the server code (TCP/IP variant) could look like: + * + * ```kotlin + * // The server implementation (could be shared between server instances connected + * // to different protocol adapters): + * + * val cli = KiloInterface().apply { + * // Suppose we want to throw this exception at the caller site, so we need to register it: + * registerError { SomeException() } + * + * // Session initialization. If you need a sessino to depend initially on the client's identity. + * // you can do it here: + * onConnected { + * // check the remoteIdentity + * session.data = if( remoteIdentity == somePublicVerifyingKey ) + * "known" + * else + * "unknown" + * } + * on(cmdSave) { session.data = it } + * on(cmdLoad) { + * session.data + * } + * on(cmdException) { + * throw TestException() + * } + * on(cmdDrop) { + * throw LocalInterface.BreakConnectionException() + * } + * } + * + * // Create the server instance that accepts incoming TCP/IP connections on all local interfaces using the + * // specified port: + * + * val server = KiloServer(cli, acceptTcpDevice(port)) { + * // This creates a new session + * Session("unknown") + * } + * ``` + * @param S the type of the server session object, returned by [sessionBuilder]. See above + * @param clientInterface the interface available for remote calls + * @param connections flow of incoming connections. Server stops when the flow is fully collected (normally + * it shouldn't) + * @param serverSecretKey the [SigningKey] used to identify this server during Kiloparsec handshake. + * @param sessionBuilder callback that creates session objects for successful incoming connections + */ class KiloServer( private val clientInterface: KiloInterface, private val connections: Flow, private val serverSecretKey: SigningKey? = null, - private val sessionBuilder: ()->S, - ): LogTag("KS:${instances.incrementAndGet()}") { + private val sessionBuilder: () -> S, +) : LogTag("KS:${instances.incrementAndGet()}") { private val job = globalLaunch { connections.collect { device -> @@ -27,22 +111,30 @@ class KiloServer( try { info { "connected ${device}" } KiloServerConnection(clientInterface, device, sessionBuilder(), serverSecretKey) - .apply { debug { "server connection is ready" }} + .apply { debug { "server connection is ready" } } .run() - } - catch(_: CancellationException) { - } - catch(cce: LocalInterface.BreakConnectionException) { + } catch (_: CancellationException) { + } catch (cce: LocalInterface.BreakConnectionException) { info { "Closed exception caught, closing (${cce.flushSendQueue}" } - } - catch (t: Throwable) { + } catch (t: Throwable) { exception { "unexpected while creating kiloclient" to t } } } } } + /** + * Stop the server and cancel all pending sessions. Unlike finishing the flow passed + * for [KiloServer.connections], it will cancel all currently active sessions. + */ fun close() { job.cancel() } + + /** + * Server is closed either if [close] was called or all [KiloServer.connections] were collected and flow was + * closed. + */ + @Suppress("unused") + val isClosed: Boolean get() = job.isCompleted } \ No newline at end of file diff --git a/src/ktorSocketTest/kotlin/TcpTest.kt b/src/ktorSocketTest/kotlin/TcpTest.kt index 3a995dc..2d44ad2 100644 --- a/src/ktorSocketTest/kotlin/TcpTest.kt +++ b/src/ktorSocketTest/kotlin/TcpTest.kt @@ -15,17 +15,17 @@ class TcpTest { @Test fun tcpTest() = runTest { initCrypto() -// Log.connectConsole(Log.Level.DEBUG) - data class Session( - var data: String - ) + // Log.connectConsole(Log.Level.DEBUG) +data class Session( + var data: String, +) val port = 27170 + Random.nextInt(1, 200) - val cmdSave by command() - val cmdLoad by command() - val cmdDrop by command() - val cmdException by command() +val cmdSave by command() +val cmdLoad by command() +val cmdDrop by command() +val cmdException by command() val cli = KiloInterface().apply { registerError { TestException() }