Fix native CLI TCP regression path

This commit is contained in:
Sergey Chernov 2026-04-09 11:00:23 +03:00
parent aa1b74620e
commit 402b8bb1b3
14 changed files with 204 additions and 22 deletions

68
examples/tcp-server.lyng Normal file
View File

@ -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..<readers.size) {
val reader = readers[i]
(reader as Deferred).await()
}
}
val srv = serveRequests(server as TcpServer)
var clients = []
for (i in 0..<N) {
//delay(500)
clients.add(launch {
try{
val socket = Net.tcpConnect(host, port)
socket.writeUtf8("ping1ping2ping3ping4ping5")
socket.flush()
socket.close()
} catch (e) {
println("ERROR [client]: " + e)
}
})
}
for (i in 0..<clients.size) {
val c = clients[i]
(c as Deferred).await()
println("client done")
}
srv.await()
delay(10000)
println("FIN")

View File

@ -18,6 +18,7 @@ slf4j = "2.0.17"
[libraries]
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
clikt-core = { module = "com.github.ajalt.clikt:clikt-core", version.ref = "clikt" }
clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" }
mordant-core = { module = "com.github.ajalt.mordant:mordant-core", version.ref = "mordant" }
mordant-jvm-jna = { module = "com.github.ajalt.mordant:mordant-jvm-jna", version.ref = "mordant" }

View File

@ -52,6 +52,12 @@ kotlin {
linuxX64 {
binaries {
executable()
all {
if (buildType == org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType.RELEASE) {
debuggable = true
optimized = false
}
}
}
}
sourceSets {
@ -63,7 +69,7 @@ kotlin {
// filesystem access into the execution Scope by default.
implementation(project(":lyngio"))
implementation(libs.okio)
implementation(libs.clikt)
implementation(libs.clikt.core)
implementation(kotlin("stdlib-common"))
// optional support for rendering markdown in help messages
// implementation(libs.clikt.markdown)

View File

@ -17,7 +17,7 @@
package net.sergeych
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CoreCliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands
@ -43,6 +43,7 @@ import net.sergeych.lyng.io.net.createNetModule
import net.sergeych.lyng.io.ws.createWsModule
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyngio.net.shutdownSystemNetEngine
import net.sergeych.lyngio.console.security.PermitAllConsoleAccessPolicy
import net.sergeych.lyngio.fs.security.PermitAllAccessPolicy
import net.sergeych.lyngio.http.security.PermitAllHttpAccessPolicy
@ -70,10 +71,18 @@ data class CommandResult(
val error: String
)
private val baseCliImportManagerDefer = globalDefer {
val manager = Script.defaultImportManager.copy().apply {
installCliModules(this)
}
manager.newStdScope()
manager
}
val baseScopeDefer = globalDefer {
Script.newScope().apply {
baseCliImportManagerDefer.await().copy().newStdScope().apply {
installCliBuiltins()
installCliModules(importManager)
addConst("ARGV", ObjList(mutableListOf()))
}
}
@ -208,23 +217,33 @@ private fun discoverLocalCliModules(entryFile: Path): List<LocalCliModule> {
.toList()
}
private fun registerLocalCliModules(manager: ImportManager, entryFile: Path) {
for (module in discoverLocalCliModules(entryFile)) {
private fun registerLocalCliModules(manager: ImportManager, modules: List<LocalCliModule>) {
for (module in modules) {
manager.addPackage(module.packageName) { scope ->
scope.eval(module.source)
}
}
}
private suspend fun newCliScope(argv: List<String>, 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<String>): Scope =
newStdScope().apply {
installCliBuiltins()
addConst("ARGV", ObjList(argv.map { ObjString(it) }.toMutableList()))
}
internal suspend fun newCliScope(argv: List<String>, 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<String>) {
@ -248,7 +267,7 @@ fun runMain(args: Array<String>) {
.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()
}
}

View File

@ -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(
"<tcp-server-regression>",
"""
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()
}
}
}

View File

@ -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) }

View File

@ -97,3 +97,5 @@ internal object UnsupportedLyngNetEngine : LyngNetEngine {
}
expect fun getSystemNetEngine(): LyngNetEngine
expect fun shutdownSystemNetEngine()

View File

@ -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)
}

View File

@ -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

View File

@ -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) }

View File

@ -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)
}

View File

@ -1,3 +1,5 @@
package net.sergeych.lyngio.net
actual fun getSystemNetEngine(): LyngNetEngine = UnsupportedLyngNetEngine
actual fun shutdownSystemNetEngine() {}

View File

@ -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<LyngSocketAddress> {
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()
}

View File

@ -151,7 +151,9 @@ 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)
}
}
}