Refactor KMP networking backends
This commit is contained in:
parent
d0aaa2c256
commit
cd7e001f41
@ -1,6 +1,6 @@
|
||||
### lyng.io.net — TCP and UDP sockets for Lyng scripts
|
||||
|
||||
This module provides minimal raw transport networking for Lyng scripts. It is implemented in `lyngio` and backed by Ktor sockets on the JVM and by Node networking APIs on JS/Node runtimes.
|
||||
This module provides minimal raw transport networking for Lyng scripts. It is implemented in `lyngio` and backed by Ktor sockets on the JVM and Linux Native, and by Node networking APIs on JS/Node runtimes.
|
||||
|
||||
> **Note:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
|
||||
|
||||
@ -164,4 +164,6 @@ The module uses `NetAccessPolicy` to authorize network operations before they ar
|
||||
- **Android:** supported via the Ktor CIO and Ktor sockets backends
|
||||
- **JS/Node:** supported for `resolve`, TCP client/server, and UDP
|
||||
- **JS/browser:** unsupported; capability checks report unavailable
|
||||
- **Other targets:** currently report unsupported; use capability checks before relying on raw sockets
|
||||
- **Linux Native:** supported via Ktor sockets
|
||||
- **Apple Native:** enabled via the shared native Ktor sockets backend; compile-verified, runtime not yet host-verified
|
||||
- **Other native targets:** currently report unsupported; use capability checks before relying on raw sockets
|
||||
|
||||
@ -86,6 +86,9 @@ kotlin {
|
||||
}
|
||||
val nativeMain by creating {
|
||||
dependsOn(commonMain)
|
||||
dependencies {
|
||||
implementation(libs.ktor.network)
|
||||
}
|
||||
}
|
||||
val darwinMain by creating {
|
||||
dependsOn(nativeMain)
|
||||
@ -117,6 +120,9 @@ kotlin {
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
}
|
||||
val linuxTest by creating {
|
||||
dependsOn(commonTest)
|
||||
}
|
||||
val iosX64Main by getting { dependsOn(iosMain) }
|
||||
val iosArm64Main by getting { dependsOn(iosMain) }
|
||||
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
|
||||
@ -124,6 +130,8 @@ kotlin {
|
||||
val mingwX64Main by getting { dependsOn(mingwMain) }
|
||||
val linuxX64Main by getting { dependsOn(linuxMain) }
|
||||
val linuxArm64Main by getting { dependsOn(linuxMain) }
|
||||
val linuxX64Test by getting { dependsOn(linuxTest) }
|
||||
val linuxArm64Test by getting { dependsOn(linuxTest) }
|
||||
|
||||
// JS: use runtime detection in jsMain to select Node vs Browser implementation
|
||||
val jsMain by getting {
|
||||
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = AndroidKtorLyngHttpEngine
|
||||
|
||||
private object AndroidKtorLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
|
||||
|
||||
@ -1,92 +1,5 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = AndroidKtorWsEngine
|
||||
|
||||
private object AndroidKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(CIO) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return AndroidLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
@Volatile
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
internal fun createKtorHttpEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
): LyngHttpEngine = KtorLyngHttpEngine(engineFactory)
|
||||
|
||||
private class KtorLyngHttpEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
) : LyngHttpEngine {
|
||||
private val clientResult = runCatching {
|
||||
HttpClient(engineFactory) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
internal fun createKtorWsEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
): LyngWsEngine = KtorLyngWsEngine(engineFactory)
|
||||
|
||||
private class KtorLyngWsEngine(
|
||||
engineFactory: HttpClientEngineFactory<HttpClientEngineConfig>,
|
||||
) : LyngWsEngine {
|
||||
private val clientResult = runCatching {
|
||||
HttpClient(engineFactory) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return KtorLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class KtorLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = DarwinLyngHttpEngine
|
||||
|
||||
private object DarwinLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Darwin) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Darwin)
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
package net.sergeych.lyngio.net
|
||||
|
||||
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
|
||||
isSupported = true,
|
||||
isTcpAvailable = true,
|
||||
isTcpServerAvailable = true,
|
||||
isUdpAvailable = true,
|
||||
)
|
||||
@ -1,91 +1,5 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.darwin.Darwin
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = DarwinKtorWsEngine
|
||||
|
||||
private object DarwinKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Darwin) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return DarwinLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class DarwinLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Darwin)
|
||||
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.js.Js
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = JsKtorLyngHttpEngine
|
||||
|
||||
private object JsKtorLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Js) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Js)
|
||||
|
||||
@ -1,91 +1,5 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.js.Js
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = JsKtorWsEngine
|
||||
|
||||
private object JsKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Js) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return JsLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class JsLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Js)
|
||||
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = JvmKtorLyngHttpEngine
|
||||
|
||||
private object JvmKtorLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(CIO)
|
||||
|
||||
@ -17,94 +17,6 @@
|
||||
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = JvmKtorWsEngine
|
||||
|
||||
private object JvmKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(CIO) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return JvmLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class JvmLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
@Volatile
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
val safeCode = code.toShort()
|
||||
session.close(CloseReason(safeCode, reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(CIO)
|
||||
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.curl.Curl
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = LinuxLyngHttpEngine
|
||||
|
||||
private object LinuxLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Curl) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(Curl)
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
package net.sergeych.lyngio.net
|
||||
|
||||
actual fun getSystemNetEngine(): LyngNetEngine = createNativeKtorNetEngine(
|
||||
isSupported = true,
|
||||
isTcpAvailable = true,
|
||||
isTcpServerAvailable = true,
|
||||
isUdpAvailable = true,
|
||||
)
|
||||
@ -1,91 +1,5 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.curl.Curl
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = LinuxKtorWsEngine
|
||||
|
||||
private object LinuxKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(Curl) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return LinuxLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class LinuxLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(Curl)
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package net.sergeych.lyngio.net
|
||||
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.io.net.createNetModule
|
||||
import net.sergeych.lyngio.net.security.PermitAllNetAccessPolicy
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NetLinuxNativeTest {
|
||||
|
||||
@Test
|
||||
fun testLinuxNativeCapabilitiesAndResolve() = runBlocking {
|
||||
val engine = getSystemNetEngine()
|
||||
|
||||
assertTrue(engine.isSupported)
|
||||
assertTrue(engine.isTcpAvailable)
|
||||
assertTrue(engine.isTcpServerAvailable)
|
||||
assertTrue(engine.isUdpAvailable)
|
||||
|
||||
val resolved = engine.resolve("127.0.0.1", 4040)
|
||||
assertEquals(1, resolved.size)
|
||||
assertEquals("127.0.0.1", resolved.single().host)
|
||||
assertEquals(4040, resolved.single().port)
|
||||
assertEquals(LyngIpVersion.IPV4, resolved.single().ipVersion)
|
||||
assertTrue(resolved.single().resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLinuxNativeLyngModuleCapabilities() = runBlocking {
|
||||
val scope = Script.newScope()
|
||||
createNetModule(PermitAllNetAccessPolicy, scope)
|
||||
|
||||
val code = """
|
||||
import lyng.io.net
|
||||
|
||||
val a: SocketAddress = Net.resolve("127.0.0.1", 4040)[0]
|
||||
[Net.isSupported(), Net.isTcpAvailable(), Net.isTcpServerAvailable(), Net.isUdpAvailable(), a.toString(), a.resolved]
|
||||
""".trimIndent()
|
||||
|
||||
val result = Compiler.compile(code).execute(scope).inspect(scope)
|
||||
assertTrue(result.contains("true,true,true,true"), result)
|
||||
assertTrue(result.contains("127.0.0.1:4040"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLinuxNativeTcpAndUdpLoopback() = runBlocking {
|
||||
val engine = getSystemNetEngine()
|
||||
|
||||
withTimeout(5_000) {
|
||||
val server = engine.tcpListen(host = "127.0.0.1", port = 0, backlog = 16, reuseAddress = true)
|
||||
val accepted = async {
|
||||
val client = server.accept()
|
||||
val text = client.read(4)?.decodeToString()
|
||||
client.writeUtf8("echo:$text")
|
||||
client.flush()
|
||||
client.close()
|
||||
server.close()
|
||||
text
|
||||
}
|
||||
|
||||
val socket = engine.tcpConnect("127.0.0.1", server.localAddress().port, timeoutMillis = 2_000, noDelay = true)
|
||||
socket.writeUtf8("ping")
|
||||
socket.flush()
|
||||
val reply = socket.read(32)?.decodeToString()
|
||||
socket.close()
|
||||
|
||||
assertEquals("ping", accepted.await())
|
||||
assertEquals("echo:ping", reply)
|
||||
}
|
||||
|
||||
withTimeout(5_000) {
|
||||
val receiver = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
|
||||
val sender = engine.udpBind(host = "127.0.0.1", port = 0, reuseAddress = true)
|
||||
|
||||
sender.send("ping".encodeToByteArray(), "127.0.0.1", receiver.localAddress().port)
|
||||
val datagram = receiver.receive(32)
|
||||
|
||||
sender.close()
|
||||
receiver.close()
|
||||
|
||||
assertEquals("ping", datagram?.data?.decodeToString())
|
||||
assertTrue((datagram?.address?.port ?: 0) > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,58 +1,5 @@
|
||||
package net.sergeych.lyngio.http
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.winhttp.WinHttp
|
||||
import io.ktor.client.plugins.timeout
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.headers
|
||||
import io.ktor.http.takeFrom
|
||||
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = MingwLyngHttpEngine
|
||||
|
||||
private object MingwLyngHttpEngine : LyngHttpEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(WinHttp) {
|
||||
expectSuccess = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun request(request: LyngHttpRequest): LyngHttpResponse {
|
||||
val httpClient = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "HTTP client is not supported")
|
||||
}
|
||||
|
||||
val response = httpClient.request {
|
||||
applyRequest(request)
|
||||
}
|
||||
return LyngHttpResponse(
|
||||
status = response.status.value,
|
||||
statusText = response.status.description,
|
||||
headers = response.headers.entries().associate { it.key to it.value.toList() },
|
||||
bodyBytes = response.body<ByteArray>(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun HttpRequestBuilder.applyRequest(request: LyngHttpRequest) {
|
||||
method = HttpMethod.parse(request.method.uppercase())
|
||||
url.takeFrom(request.url)
|
||||
headers {
|
||||
request.headers.forEach { (name, value) -> append(name, value) }
|
||||
}
|
||||
request.timeoutMillis?.let { timeout { requestTimeoutMillis = it } }
|
||||
when {
|
||||
request.bodyBytes != null && request.bodyText != null ->
|
||||
throw IllegalArgumentException("Only one of bodyText or bodyBytes may be set")
|
||||
request.bodyBytes != null -> setBody(request.bodyBytes)
|
||||
request.bodyText != null -> setBody(request.bodyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun getSystemHttpEngine(): LyngHttpEngine = createKtorHttpEngine(WinHttp)
|
||||
|
||||
@ -1,91 +1,5 @@
|
||||
package net.sergeych.lyngio.ws
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.winhttp.WinHttp
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.DefaultWebSocketSession
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = MingwKtorWsEngine
|
||||
|
||||
private object MingwKtorWsEngine : LyngWsEngine {
|
||||
private val clientResult by lazy {
|
||||
runCatching {
|
||||
HttpClient(WinHttp) {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val isSupported: Boolean
|
||||
get() = clientResult.isSuccess
|
||||
|
||||
override suspend fun connect(url: String, headers: Map<String, String>): LyngWsSession {
|
||||
val client = clientResult.getOrElse {
|
||||
throw UnsupportedOperationException(it.message ?: "WebSocket client is not supported")
|
||||
}
|
||||
val session = client.webSocketSession {
|
||||
url(url)
|
||||
headers.forEach { (name, value) -> header(name, value) }
|
||||
}
|
||||
return MingwLyngWsSession(url, session)
|
||||
}
|
||||
}
|
||||
|
||||
private class MingwLyngWsSession(
|
||||
private val targetUrl: String,
|
||||
private val session: DefaultWebSocketSession,
|
||||
) : LyngWsSession {
|
||||
private var closed = false
|
||||
|
||||
override fun isOpen(): Boolean = !closed
|
||||
|
||||
override fun url(): String = targetUrl
|
||||
|
||||
override suspend fun sendText(text: String) {
|
||||
ensureOpen()
|
||||
session.send(text)
|
||||
}
|
||||
|
||||
override suspend fun sendBytes(data: ByteArray) {
|
||||
ensureOpen()
|
||||
session.send(data)
|
||||
}
|
||||
|
||||
override suspend fun receive(): LyngWsMessage? {
|
||||
if (closed) return null
|
||||
val frame = try {
|
||||
session.incoming.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
closed = true
|
||||
return null
|
||||
}
|
||||
return when (frame) {
|
||||
is Frame.Text -> LyngWsMessage(isText = true, text = frame.readText())
|
||||
is Frame.Binary -> LyngWsMessage(isText = false, data = frame.data.copyOf())
|
||||
is Frame.Close -> {
|
||||
closed = true
|
||||
null
|
||||
}
|
||||
else -> receive()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun close(code: Int, reason: String) {
|
||||
if (closed) return
|
||||
closed = true
|
||||
session.close(CloseReason(code.toShort(), reason))
|
||||
}
|
||||
|
||||
private fun ensureOpen() {
|
||||
if (closed) throw IllegalStateException("websocket session is closed")
|
||||
}
|
||||
}
|
||||
actual fun getSystemWsEngine(): LyngWsEngine = createKtorWsEngine(WinHttp)
|
||||
|
||||
@ -0,0 +1,216 @@
|
||||
package net.sergeych.lyngio.net
|
||||
|
||||
import io.ktor.network.selector.SelectorManager
|
||||
import io.ktor.network.sockets.BoundDatagramSocket
|
||||
import io.ktor.network.sockets.Datagram
|
||||
import io.ktor.network.sockets.InetSocketAddress
|
||||
import io.ktor.network.sockets.ServerSocket
|
||||
import io.ktor.network.sockets.Socket
|
||||
import io.ktor.network.sockets.SocketAddress
|
||||
import io.ktor.network.sockets.aSocket
|
||||
import io.ktor.network.sockets.isClosed
|
||||
import io.ktor.network.sockets.openReadChannel
|
||||
import io.ktor.network.sockets.openWriteChannel
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.ByteWriteChannel
|
||||
import io.ktor.utils.io.readAvailable
|
||||
import io.ktor.utils.io.readUTF8Line
|
||||
import io.ktor.utils.io.writeFully
|
||||
import io.ktor.utils.io.writeStringUtf8
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.io.Buffer
|
||||
import kotlinx.io.readByteArray
|
||||
|
||||
internal fun createNativeKtorNetEngine(
|
||||
isSupported: Boolean,
|
||||
isTcpAvailable: Boolean,
|
||||
isTcpServerAvailable: Boolean,
|
||||
isUdpAvailable: Boolean,
|
||||
): LyngNetEngine = NativeKtorNetEngine(
|
||||
isSupported = isSupported,
|
||||
isTcpAvailable = isTcpAvailable,
|
||||
isTcpServerAvailable = isTcpServerAvailable,
|
||||
isUdpAvailable = isUdpAvailable,
|
||||
)
|
||||
|
||||
private class NativeKtorNetEngine(
|
||||
override val isSupported: Boolean,
|
||||
override val isTcpAvailable: Boolean,
|
||||
override val isTcpServerAvailable: Boolean,
|
||||
override val isUdpAvailable: Boolean,
|
||||
) : LyngNetEngine {
|
||||
private val selectorManager: SelectorManager by lazy { SelectorManager(Dispatchers.Default) }
|
||||
|
||||
override suspend fun resolve(host: String, port: Int): List<LyngSocketAddress> {
|
||||
val rawAddress = InetSocketAddress(host, port).resolveAddress()
|
||||
?: throw IllegalStateException("Failed to resolve address for $host")
|
||||
return listOf(
|
||||
LyngSocketAddress(
|
||||
host = rawAddress.toIpHostString(),
|
||||
port = port,
|
||||
ipVersion = rawAddress.toLyngIpVersion(),
|
||||
resolved = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun tcpConnect(
|
||||
host: String,
|
||||
port: Int,
|
||||
timeoutMillis: Long?,
|
||||
noDelay: Boolean,
|
||||
): LyngTcpSocket {
|
||||
val connectBlock: suspend () -> Socket = {
|
||||
aSocket(selectorManager).tcp().connect(host, port) {
|
||||
this.noDelay = noDelay
|
||||
}
|
||||
}
|
||||
val socket = if (timeoutMillis != null) withTimeout(timeoutMillis) { connectBlock() } else connectBlock()
|
||||
return NativeLyngTcpSocket(socket)
|
||||
}
|
||||
|
||||
override suspend fun tcpListen(
|
||||
host: String?,
|
||||
port: Int,
|
||||
backlog: Int,
|
||||
reuseAddress: Boolean,
|
||||
): LyngTcpServer {
|
||||
val bindHost = host ?: "0.0.0.0"
|
||||
val server = aSocket(selectorManager).tcp().bind(bindHost, port) {
|
||||
backlogSize = backlog
|
||||
this.reuseAddress = reuseAddress
|
||||
}
|
||||
return NativeLyngTcpServer(server)
|
||||
}
|
||||
|
||||
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) {
|
||||
this.reuseAddress = reuseAddress
|
||||
}
|
||||
return NativeLyngUdpSocket(socket)
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeLyngTcpSocket(
|
||||
private val socket: Socket,
|
||||
) : LyngTcpSocket {
|
||||
private val input: ByteReadChannel by lazy { socket.openReadChannel() }
|
||||
private val output: ByteWriteChannel by lazy { socket.openWriteChannel(autoFlush = true) }
|
||||
|
||||
override fun isOpen(): Boolean = !socket.isClosed
|
||||
|
||||
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
|
||||
|
||||
override fun remoteAddress(): LyngSocketAddress = socket.remoteAddress.toLyngSocketAddress(resolved = true)
|
||||
|
||||
override suspend fun read(maxBytes: Int): ByteArray? {
|
||||
if (!input.awaitContent(1)) return null
|
||||
val buffer = ByteArray(maxBytes)
|
||||
val count = input.readAvailable(buffer, 0, maxBytes)
|
||||
return when {
|
||||
count <= 0 -> null
|
||||
count == maxBytes -> buffer
|
||||
else -> buffer.copyOf(count)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun readLine(): String? = input.readUTF8Line()
|
||||
|
||||
override suspend fun write(data: ByteArray) {
|
||||
output.writeFully(data, 0, data.size)
|
||||
}
|
||||
|
||||
override suspend fun writeUtf8(text: String) {
|
||||
output.writeStringUtf8(text)
|
||||
}
|
||||
|
||||
override suspend fun flush() {
|
||||
output.flush()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeLyngTcpServer(
|
||||
private val server: ServerSocket,
|
||||
) : LyngTcpServer {
|
||||
override fun isOpen(): Boolean = !server.isClosed
|
||||
|
||||
override fun localAddress(): LyngSocketAddress = server.localAddress.toLyngSocketAddress(resolved = true)
|
||||
|
||||
override suspend fun accept(): LyngTcpSocket = NativeLyngTcpSocket(server.accept())
|
||||
|
||||
override fun close() {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeLyngUdpSocket(
|
||||
private val socket: BoundDatagramSocket,
|
||||
) : LyngUdpSocket {
|
||||
override fun isOpen(): Boolean = !socket.isClosed
|
||||
|
||||
override fun localAddress(): LyngSocketAddress = socket.localAddress.toLyngSocketAddress(resolved = true)
|
||||
|
||||
override suspend fun receive(maxBytes: Int): LyngDatagram? {
|
||||
val datagram = try {
|
||||
socket.receive()
|
||||
} catch (e: Throwable) {
|
||||
if (!isOpen()) return null
|
||||
throw e
|
||||
}
|
||||
val bytes = datagram.packet.readByteArray().let {
|
||||
if (it.size <= maxBytes) it else it.copyOf(maxBytes)
|
||||
}
|
||||
return LyngDatagram(bytes, datagram.address.toLyngSocketAddress(resolved = true))
|
||||
}
|
||||
|
||||
override suspend fun send(data: ByteArray, host: String, port: Int) {
|
||||
val packet = Buffer()
|
||||
packet.write(data)
|
||||
socket.send(Datagram(packet, InetSocketAddress(host, port)))
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun SocketAddress.toLyngSocketAddress(resolved: Boolean): LyngSocketAddress {
|
||||
val inetAddress = this as? InetSocketAddress
|
||||
if (inetAddress != null) {
|
||||
val rawAddress = inetAddress.resolveAddress()
|
||||
val host = rawAddress?.toIpHostString() ?: inetAddress.hostname
|
||||
return LyngSocketAddress(
|
||||
host = host,
|
||||
port = inetAddress.port,
|
||||
ipVersion = rawAddress?.toLyngIpVersion()
|
||||
?: if (host.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
|
||||
resolved = resolved,
|
||||
)
|
||||
}
|
||||
|
||||
val rendered = toString()
|
||||
return LyngSocketAddress(
|
||||
host = rendered,
|
||||
port = 0,
|
||||
ipVersion = if (rendered.contains(':')) LyngIpVersion.IPV6 else LyngIpVersion.IPV4,
|
||||
resolved = resolved,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ByteArray.toLyngIpVersion(): LyngIpVersion = if (size == 16) LyngIpVersion.IPV6 else LyngIpVersion.IPV4
|
||||
|
||||
private fun ByteArray.toIpHostString(): String = when (size) {
|
||||
4 -> joinToString(".") { (it.toInt() and 0xff).toString() }
|
||||
16 -> (0 until 8).joinToString(":") { index ->
|
||||
val hi = this[index * 2].toInt() and 0xff
|
||||
val lo = this[index * 2 + 1].toInt() and 0xff
|
||||
((hi shl 8) or lo).toString(16)
|
||||
}
|
||||
else -> error("Unsupported IP address length: $size")
|
||||
}
|
||||
205
notes/networking_handoff_2026-04-02.md
Normal file
205
notes/networking_handoff_2026-04-02.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Networking Handoff
|
||||
|
||||
Date: 2026-04-02
|
||||
Commit: `5346d15` (`Add KMP networking backends`)
|
||||
|
||||
## Scope completed
|
||||
|
||||
The `lyngio` networking work now provides a uniform Lyng-facing API with capability probes and platform-specific implementations.
|
||||
|
||||
Implemented modules:
|
||||
|
||||
- `lyng.io.http`
|
||||
- `lyng.io.ws`
|
||||
- `lyng.io.net`
|
||||
|
||||
## Current support matrix
|
||||
|
||||
### HTTP / HTTPS
|
||||
|
||||
- JVM: supported
|
||||
- Android: supported
|
||||
- JS: supported
|
||||
- Linux Native: supported
|
||||
- Windows Native (`mingwX64`): supported
|
||||
- Apple Native: compile-verified on this Linux host
|
||||
|
||||
### WS / WSS
|
||||
|
||||
- JVM: supported
|
||||
- Android: supported
|
||||
- JS: supported
|
||||
- Linux Native: supported
|
||||
- Windows Native (`mingwX64`): supported
|
||||
- Apple Native: compile-verified on this Linux host
|
||||
|
||||
### Raw networking (`lyng.io.net`)
|
||||
|
||||
- JVM: supported
|
||||
- Android: supported
|
||||
- JS/Node: supported
|
||||
- JS/browser: unsupported by capability probe
|
||||
- Linux Native: supported
|
||||
- Apple Native: enabled via shared native backend; compile-verified, runtime not yet host-verified
|
||||
- Other Native targets: intentionally still unsupported
|
||||
|
||||
## Important design decisions
|
||||
|
||||
- Ktor is the backend for all currently implemented networking.
|
||||
- API is uniform across targets; platform variance is exposed through capability checks such as:
|
||||
- `Http.isSupported()`
|
||||
- `Ws.isSupported()`
|
||||
- `Net.isSupported()`
|
||||
- `Net.isTcpAvailable()`
|
||||
- `Net.isTcpServerAvailable()`
|
||||
- `Net.isUdpAvailable()`
|
||||
- Native support was restricted to what matches Ktor client-engine support:
|
||||
- Darwin for Apple Native
|
||||
- Curl for Linux Native
|
||||
- WinHttp for Windows Native
|
||||
- Native raw sockets use a shared Ktor socket implementation for Linux and Darwin source sets.
|
||||
- Capability probes are enabled on Linux Native and Apple Native; Apple Native is compile-verified but not yet runtime-tested on a macOS host.
|
||||
|
||||
## Documentation and tests status
|
||||
|
||||
Docs updated:
|
||||
|
||||
- `docs/lyng.io.http.md`
|
||||
- `docs/lyng.io.ws.md`
|
||||
- `docs/lyng.io.net.md`
|
||||
|
||||
Verified docs/tests:
|
||||
|
||||
- HTTP/HTTPS docs are covered with extracted markdown tests on JVM.
|
||||
- WS/WSS docs are covered with extracted markdown tests on JVM.
|
||||
- JS/Node has both engine-level and Lyng-module-level tests for raw networking.
|
||||
|
||||
## Key implementation files
|
||||
|
||||
Shared:
|
||||
|
||||
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/http/LyngHttp.kt`
|
||||
- `lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/LyngHttpModule.kt`
|
||||
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/ws/LyngWs.kt`
|
||||
- `lyngio/src/commonMain/kotlin/net/sergeych/lyngio/net/LyngNet.kt`
|
||||
- `lyngio/build.gradle.kts`
|
||||
- `gradle/libs.versions.toml`
|
||||
|
||||
Platform HTTP:
|
||||
|
||||
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/http/PlatformJvm.kt`
|
||||
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/http/PlatformJs.kt`
|
||||
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/http/PlatformAndroid.kt`
|
||||
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/http/PlatformDarwin.kt`
|
||||
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/http/PlatformLinux.kt`
|
||||
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/http/PlatformMingw.kt`
|
||||
|
||||
Platform WS:
|
||||
|
||||
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/ws/PlatformJvm.kt`
|
||||
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/ws/PlatformJs.kt`
|
||||
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/ws/PlatformAndroid.kt`
|
||||
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/ws/PlatformDarwin.kt`
|
||||
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/ws/PlatformLinux.kt`
|
||||
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/ws/PlatformMingw.kt`
|
||||
|
||||
Platform raw net:
|
||||
|
||||
- `lyngio/src/jvmMain/kotlin/net/sergeych/lyngio/net/PlatformJvm.kt`
|
||||
- `lyngio/src/jsMain/kotlin/net/sergeych/lyngio/net/PlatformJs.kt`
|
||||
- `lyngio/src/androidMain/kotlin/net/sergeych/lyngio/net/PlatformAndroid.kt`
|
||||
- `lyngio/src/nativeMain/kotlin/net/sergeych/lyngio/net/NativeKtorNetEngine.kt`
|
||||
- `lyngio/src/linuxMain/kotlin/net/sergeych/lyngio/net/PlatformLinux.kt`
|
||||
- `lyngio/src/darwinMain/kotlin/net/sergeych/lyngio/net/PlatformDarwin.kt`
|
||||
- `lyngio/src/mingwMain/kotlin/net/sergeych/lyngio/net/PlatformMingw.kt`
|
||||
|
||||
JS tests:
|
||||
|
||||
- `lyngio/src/jsTest/kotlin/net/sergeych/lyngio/PlatformCapabilityJsTest.kt`
|
||||
- `lyngio/src/jsTest/kotlin/net/sergeych/lyngio/NetJsNodeTest.kt`
|
||||
- `lyngio/src/jsTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleJsNodeTest.kt`
|
||||
|
||||
JVM tests:
|
||||
|
||||
- `lyngio/src/jvmTest/kotlin/LyngioBookTest.kt`
|
||||
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/LyngHttpModuleTest.kt`
|
||||
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/ws/LyngWsModuleTest.kt`
|
||||
- `lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/net/LyngNetModuleTest.kt`
|
||||
|
||||
Linux Native tests:
|
||||
|
||||
- `lyngio/src/linuxTest/kotlin/net/sergeych/lyngio/net/NetLinuxNativeTest.kt`
|
||||
|
||||
## Verification already run
|
||||
|
||||
JVM / docs:
|
||||
|
||||
- `./gradlew :lyngio:jvmTest --tests LyngioBookTest`
|
||||
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.http.LyngHttpModuleTest`
|
||||
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.ws.LyngWsModuleTest`
|
||||
- `./gradlew :lyngio:jvmTest --tests net.sergeych.lyng.io.net.LyngNetModuleTest`
|
||||
|
||||
JS:
|
||||
|
||||
- `./gradlew :lyngio:compileKotlinJs`
|
||||
- `./gradlew :lyngio:compileTestKotlinJs`
|
||||
- `./gradlew :lyngio:jsNodeTest`
|
||||
- `./gradlew kotlinUpgradeYarnLock`
|
||||
|
||||
Android:
|
||||
|
||||
- `./gradlew :lyngio:compileDebugKotlinAndroid`
|
||||
- `./gradlew :lyngio:compileReleaseKotlinAndroid`
|
||||
|
||||
Native:
|
||||
|
||||
- `./gradlew :lyngio:compileKotlinLinuxX64`
|
||||
- `./gradlew :lyngio:compileKotlinLinuxArm64`
|
||||
- `./gradlew :lyngio:compileKotlinMingwX64`
|
||||
- `./gradlew :lyngio:compileKotlinIosX64`
|
||||
- `./gradlew :lyngio:compileKotlinIosArm64`
|
||||
- `./gradlew :lyngio:compileKotlinIosSimulatorArm64`
|
||||
- `./gradlew :lyngio:compileKotlinMacosArm64`
|
||||
- `./gradlew :lyngio:compileTestKotlinLinuxX64`
|
||||
- `./gradlew :lyngio:compileTestKotlinLinuxArm64`
|
||||
- `./gradlew :lyngio:linkDebugTestLinuxX64`
|
||||
- `./gradlew :lyngio:linuxX64Test`
|
||||
- `./gradlew :lyngio:linuxX64Test --tests net.sergeych.lyngio.net.NetLinuxNativeTest`
|
||||
- `./gradlew :lyngio:linuxX64Test --tests net.sergeych.lyngio.net.NetLinuxNativeTest.testLinuxNativeTcpAndUdpLoopback`
|
||||
- `./lyngio/build/bin/linuxX64/debugTest/test.kexe --ktest_filter='net.sergeych.lyngio.net.NetLinuxNativeTest.*'`
|
||||
|
||||
## Known intentional gaps
|
||||
|
||||
- Native raw sockets are enabled on Linux Native and Apple Native.
|
||||
- Apple Native raw networking is enabled based on shared-backend compile verification; runtime verification on macOS is still pending.
|
||||
- No Android device/instrumented runtime tests were added; only compile verification was done.
|
||||
|
||||
## Worktree state after commit
|
||||
|
||||
Current HEAD:
|
||||
|
||||
- `5346d15` `Add KMP networking backends`
|
||||
|
||||
Unrelated remaining change:
|
||||
|
||||
- `examples/tetris_console.lyng`
|
||||
|
||||
That file was not touched by the networking work and was intentionally left out of the commit.
|
||||
|
||||
## Recommended next steps
|
||||
|
||||
1. Add Native raw socket support only per target that compiles and passes a smoke test.
|
||||
2. Keep capability probes `false` on non-Linux Native targets until each raw-socket backend is proven.
|
||||
3. If Apple Native work continues, compile Darwin targets on a macOS host before claiming support.
|
||||
4. Run a macOS-hosted runtime smoke test when available to verify the already-enabled Darwin backend.
|
||||
5. Optionally add Android runtime tests later; compile-only verification exists now.
|
||||
|
||||
## Suggested first task for the next chat
|
||||
|
||||
Continue Native raw `lyng.io.net` incrementally from the verified Linux baseline:
|
||||
|
||||
- keep Linux Native enabled
|
||||
- keep Apple Native enabled unless runtime verification disproves the shared-backend assumption
|
||||
- keep other Native targets capability-gated off until compiled and smoke-tested
|
||||
- use only `ktor-network` support that actually compiles
|
||||
- do not change the Lyng-facing API
|
||||
Loading…
x
Reference in New Issue
Block a user