From 402b8bb1b3cfac3f9fdc8354da27eeeb2cd1149a Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 9 Apr 2026 11:00:23 +0300 Subject: [PATCH] Fix native CLI TCP regression path --- examples/tcp-server.lyng | 68 +++++++++++++++++++ gradle/libs.versions.toml | 1 + lyng/build.gradle.kts | 8 ++- lyng/src/commonMain/kotlin/Common.kt | 46 +++++++++---- .../sergeych/CliTcpServerRegressionTest.kt | 51 ++++++++++++++ .../sergeych/lyngio/net/PlatformAndroid.kt | 2 + .../kotlin/net/sergeych/lyngio/net/LyngNet.kt | 2 + .../net/sergeych/lyngio/net/PlatformDarwin.kt | 8 ++- .../net/sergeych/lyngio/net/PlatformJs.kt | 2 + .../net/sergeych/lyngio/net/PlatformJvm.kt | 2 + .../net/sergeych/lyngio/net/PlatformLinux.kt | 8 ++- .../net/sergeych/lyngio/net/PlatformMingw.kt | 2 + .../lyngio/net/NativeKtorNetEngine.kt | 20 ++++-- .../net/sergeych/lyng/pacman/ImportManager.kt | 6 +- 14 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 examples/tcp-server.lyng create mode 100644 lyng/src/commonTest/kotlin/net/sergeych/CliTcpServerRegressionTest.kt diff --git a/examples/tcp-server.lyng b/examples/tcp-server.lyng new file mode 100644 index 0000000..a3538a6 --- /dev/null +++ b/examples/tcp-server.lyng @@ -0,0 +1,68 @@ +import lyng.buffer +import lyng.io.net + +val host = "127.0.0.1" +val port = 8092 +val N = 5 +val server = Net.tcpListen(port, host) +println("start tcp server at $host:$port") + +fun serveClient(client: TcpSocket) = launch { + try { + while (true) { + val data = client.read() + if (data == null) break + var line = (data as Buffer).decodeUtf8() + line = "[" + client.remoteAddress() + "]> " + line + println(line) + } + } catch (e) { + println("ERROR [reader]: " + e) + } +} + +fun serveRequests(server: TcpServer) = launch { + val readers = [] + try { + for (i in 0..<5) { + val client = server.accept() + println("accept new connection: " + client.remoteAddress()) + readers.add(serveClient(client as TcpSocket)) + } + } catch (e) { + println("ERROR [listener]: " + e) + } finally { + server.close() + } + for (i in 0.. { .toList() } -private fun registerLocalCliModules(manager: ImportManager, entryFile: Path) { - for (module in discoverLocalCliModules(entryFile)) { +private fun registerLocalCliModules(manager: ImportManager, modules: List) { + for (module in modules) { manager.addPackage(module.packageName) { scope -> scope.eval(module.source) } } } -private suspend fun newCliScope(argv: List, entryFileName: String? = null): Scope { - val manager = baseScopeDefer.await().importManager.copy() - if (entryFileName != null) { - registerLocalCliModules(manager, canonicalPath(entryFileName.toPath())) - } - return manager.newStdScope().apply { +private suspend fun ImportManager.newCliScope(argv: List): Scope = + newStdScope().apply { installCliBuiltins() addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList())) } + +internal suspend fun newCliScope(argv: List, entryFileName: String? = null): Scope { + val baseManager = baseCliImportManagerDefer.await() + if (entryFileName == null) { + return baseManager.newCliScope(argv) + } + val entryFile = canonicalPath(entryFileName.toPath()) + val localModules = discoverLocalCliModules(entryFile) + if (localModules.isEmpty()) { + return baseManager.newCliScope(argv) + } + val manager = baseManager.copy() + registerLocalCliModules(manager, localModules) + return manager.newCliScope(argv) } fun runMain(args: Array) { @@ -248,7 +267,7 @@ fun runMain(args: Array) { .main(args) } -private class Fmt : CliktCommand(name = "fmt") { +private class Fmt : CoreCliktCommand(name = "fmt") { private val checkOnly by option("--check", help = "Check only; print files that would change").flag() private val inPlace by option("-i", "--in-place", help = "Write changes back to files").flag() private val enableSpacing by option("--spacing", help = "Apply spacing normalization").flag() @@ -306,7 +325,7 @@ private class Fmt : CliktCommand(name = "fmt") { } } -private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CliktCommand() { +private class Lyng(val launcher: (suspend () -> Unit) -> Unit) : CoreCliktCommand() { override val invokeWithoutSubcommand = true override val printHelpOnEmptyArgs = true @@ -382,6 +401,7 @@ suspend fun executeSource(source: Source, initialScope: Scope? = null) { evalOnCliDispatcher(session, source) } finally { session.cancelAndJoin() + shutdownSystemNetEngine() } } diff --git a/lyng/src/commonTest/kotlin/net/sergeych/CliTcpServerRegressionTest.kt b/lyng/src/commonTest/kotlin/net/sergeych/CliTcpServerRegressionTest.kt new file mode 100644 index 0000000..8bd0dc9 --- /dev/null +++ b/lyng/src/commonTest/kotlin/net/sergeych/CliTcpServerRegressionTest.kt @@ -0,0 +1,51 @@ +package net.sergeych + +import kotlinx.coroutines.runBlocking +import net.sergeych.lyng.EvalSession +import net.sergeych.lyng.Source +import net.sergeych.lyng.obj.ObjString +import kotlin.test.Test +import kotlin.test.assertEquals + +class CliTcpServerRegressionTest { + + @Test + fun reducedTcpServerExampleRunsWithCopiedCliImportManager() = runBlocking { + val cliScope = newCliScope(emptyList()) + val session = EvalSession(cliScope) + + try { + val result = evalOnCliDispatcher( + session, + Source( + "", + """ + import lyng.buffer + import lyng.io.net + + val host = "127.0.0.1" + val server = Net.tcpListen(0, host) + val port = server.localAddress().port + val accepted = launch { + val client = server.accept() + val line = (client.read(4) as Buffer).decodeUtf8() + client.close() + server.close() + line + } + + val socket = Net.tcpConnect(host, port) + socket.writeUtf8("ping") + socket.flush() + socket.close() + accepted.await() + """.trimIndent() + ) + ) + + assertEquals("ping", (result as ObjString).value) + } finally { + session.cancelAndJoin() + } + } +} diff --git a/lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt b/lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt index 163efd3..4bc7f6e 100644 --- a/lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt +++ b/lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt @@ -28,6 +28,8 @@ import java.net.InetAddress actual fun getSystemNetEngine(): LyngNetEngine = AndroidKtorNetEngine +actual fun shutdownSystemNetEngine() {} + private object AndroidKtorNetEngine : LyngNetEngine { private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) } diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt index 66307ec..544d965 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt @@ -97,3 +97,5 @@ internal object UnsupportedLyngNetEngine : LyngNetEngine { } expect fun getSystemNetEngine(): LyngNetEngine + +expect fun shutdownSystemNetEngine() diff --git a/lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt b/lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt index d9cddfc..85b4318 100644 --- a/lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt +++ b/lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt @@ -1,8 +1,14 @@ package net.sergeych.lyngio.net -actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine( +private val systemNetEngine: LyngNetEngine = createNativeKtorNetEngine( isSupported = true, isTcpAvailable = true, isTcpServerAvailable = true, isUdpAvailable = true, ) + +actual fun getSystemNetEngine(): LyngNetEngine = systemNetEngine + +actual fun shutdownSystemNetEngine() { + shutdownNativeKtorNetEngine(systemNetEngine) +} diff --git a/lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt b/lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt index 1a31dd8..b656a66 100644 --- a/lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt +++ b/lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt @@ -13,6 +13,8 @@ import org.khronos.webgl.Uint8Array actual fun getSystemNetEngine(): LyngNetEngine = jsNodeNetEngineOrNull ?: UnsupportedLyngNetEngine +actual fun shutdownSystemNetEngine() {} + private val jsNodeNetEngineOrNull: LyngNetEngine? by lazy { if (!isNodeRuntime()) return@lazy null val net = requireNodeModule("net") ?: return@lazy null diff --git a/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt b/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt index 22cdae5..f06a832 100644 --- a/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt +++ b/lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt @@ -45,6 +45,8 @@ import java.net.InetAddress actual fun getSystemNetEngine(): LyngNetEngine = JvmKtorNetEngine +actual fun shutdownSystemNetEngine() {} + private object JvmKtorNetEngine : LyngNetEngine { private val selectorManager: SelectorManager by lazy { ActorSelectorManager(Dispatchers.IO) } diff --git a/lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt b/lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt index d9cddfc..85b4318 100644 --- a/lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt +++ b/lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt @@ -1,8 +1,14 @@ package net.sergeych.lyngio.net -actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine( +private val systemNetEngine: LyngNetEngine = createNativeKtorNetEngine( isSupported = true, isTcpAvailable = true, isTcpServerAvailable = true, isUdpAvailable = true, ) + +actual fun getSystemNetEngine(): LyngNetEngine = systemNetEngine + +actual fun shutdownSystemNetEngine() { + shutdownNativeKtorNetEngine(systemNetEngine) +} diff --git a/lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt b/lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt index 7a10ec5..5912803 100644 --- a/lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt +++ b/lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt @@ -1,3 +1,5 @@ package net.sergeych.lyngio.net actual fun getSystemNetEngine(): LyngNetEngine = UnsupportedLyngNetEngine + +actual fun shutdownSystemNetEngine() {} diff --git a/lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt b/lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt index 2233a71..944b989 100644 --- a/lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt +++ b/lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt @@ -40,7 +40,10 @@ private class NativeKtorNetEngine( override val isTcpServerAvailable: Boolean, override val isUdpAvailable: Boolean, ) : LyngNetEngine { - private val selectorManager: SelectorManager by lazy { SelectorManager(Dispatchers.Default) } + private var selectorManager: SelectorManager? = null + + private fun selectorManager(): SelectorManager = + selectorManager ?: SelectorManager(Dispatchers.Default).also { selectorManager = it } override suspend fun resolve(host: String, port: Int): List { val rawAddress = InetSocketAddress(host, port).resolveAddress() @@ -62,7 +65,7 @@ private class NativeKtorNetEngine( noDelay: Boolean, ): LyngTcpSocket { val connectBlock: suspend () -> Socket = { - aSocket(selectorManager).tcp().connect(host, port) { + aSocket(selectorManager()).tcp().connect(host, port) { this.noDelay = noDelay } } @@ -77,7 +80,7 @@ private class NativeKtorNetEngine( reuseAddress: Boolean, ): LyngTcpServer { val bindHost = host ?: "0.0.0.0" - val server = aSocket(selectorManager).tcp().bind(bindHost, port) { + val server = aSocket(selectorManager()).tcp().bind(bindHost, port) { backlogSize = backlog this.reuseAddress = reuseAddress } @@ -86,11 +89,16 @@ private class NativeKtorNetEngine( override suspend fun udpBind(host: String?, port: Int, reuseAddress: Boolean): LyngUdpSocket { val bindHost = host ?: "0.0.0.0" - val socket = aSocket(selectorManager).udp().bind(bindHost, port) { + val socket = aSocket(selectorManager()).udp().bind(bindHost, port) { this.reuseAddress = reuseAddress } return NativeLyngUdpSocket(socket) } + + fun shutdown() { + selectorManager?.close() + selectorManager = null + } } private class NativeLyngTcpSocket( @@ -214,3 +222,7 @@ private fun ByteArray.toIpHostString(): String = when (size) { } else -> error("Unsupported IP address length: $size") } + +internal fun shutdownNativeKtorNetEngine(engine: LyngNetEngine) { + (engine as? NativeKtorNetEngine)?.shutdown() +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportManager.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportManager.kt index b464814..d6956bd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportManager.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/pacman/ImportManager.kt @@ -151,8 +151,10 @@ class ImportManager( fun copy(): ImportManager = op.withLock { ImportManager(rootScope, securityManager).apply { - imports.putAll(this@ImportManager.imports) + for ((name, entry) in this@ImportManager.imports) { + imports[name] = Entry(entry.packageName, entry.builder, entry.cachedScope) + } } } -} \ No newline at end of file +}