0.6.* started:
kotlin upgrade to 2.1.0 ktor updgared to 3.1.6 less debug noise yet no wasmJS
This commit is contained in:
parent
0ff27e6de9
commit
c1bd6f09a9
@ -6,13 +6,14 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.5.4-SNAPSHOT"
|
version = "0.6.1-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
maven("https://maven.universablockchain.com/")
|
maven("https://maven.universablockchain.com/")
|
||||||
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
|
||||||
|
maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@ -32,8 +33,10 @@ kotlin {
|
|||||||
// macosX64()
|
// macosX64()
|
||||||
// macosX64()
|
// macosX64()
|
||||||
mingwX64()
|
mingwX64()
|
||||||
|
// @OptIn(ExperimentalWasmDsl::class)
|
||||||
|
// wasmJs()
|
||||||
|
|
||||||
val ktor_version = "2.3.12"
|
val ktor_version = "3.1.0"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
all {
|
all {
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package net.sergeych.kiloparsec
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplatform atomically mutable value to be used in [kotlinx.coroutines],
|
||||||
|
* with suspending mutating operations, see [mutate].
|
||||||
|
*
|
||||||
|
* Actual value can be either changed in a block of [mutate] when
|
||||||
|
* new value _depends on the current value_ or with [reset].
|
||||||
|
*
|
||||||
|
* [value] getter is suspended because it waits until the mutation finishes
|
||||||
|
*/
|
||||||
|
open class AtomicAsyncValue<T>(initialValue: T) {
|
||||||
|
private var actualValue = initialValue
|
||||||
|
private val access = Mutex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the value: get the current and set to the returned, all in the
|
||||||
|
* atomic suspend operation. All other mutating requests including assigning to [value]
|
||||||
|
* will be blocked and queued.
|
||||||
|
* @return result of the mutation. Note that immediate call to property [value]
|
||||||
|
* could already return modified bu some other thread value!
|
||||||
|
*/
|
||||||
|
suspend fun mutate(mutator: suspend (T) -> T): T = access.withLock {
|
||||||
|
actualValue = mutator(actualValue)
|
||||||
|
actualValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomic get or set the value. Atomic get means if there is a [mutate] in progress
|
||||||
|
* it will wait until the mutation finishes and then return the correct result.
|
||||||
|
*/
|
||||||
|
suspend fun value() = access.withLock { actualValue }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the new value without checking it. Shortcut to
|
||||||
|
* ```mutate { value = newValue }```
|
||||||
|
*/
|
||||||
|
suspend fun reset(value: T) = mutate { value }
|
||||||
|
}
|
@ -15,7 +15,7 @@ import net.sergeych.kiloparsec.KiloServerConnection
|
|||||||
import net.sergeych.kiloparsec.RemoteInterface
|
import net.sergeych.kiloparsec.RemoteInterface
|
||||||
import net.sergeych.mp_logger.*
|
import net.sergeych.mp_logger.*
|
||||||
import net.sergeych.tools.AtomicCounter
|
import net.sergeych.tools.AtomicCounter
|
||||||
import java.time.Duration
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ktor-based websocket server.
|
* Create a ktor-based websocket server.
|
||||||
@ -32,7 +32,6 @@ import java.time.Duration
|
|||||||
* session could be transport specific.
|
* session could be transport specific.
|
||||||
*
|
*
|
||||||
* @param localInterface where the actual work is performed.
|
* @param localInterface where the actual work is performed.
|
||||||
* @param timeout how long to wait for the connection to be established.
|
|
||||||
* @param path default http path to the websocket.
|
* @param path default http path to the websocket.
|
||||||
* @param serverKey optional key to authenticate the connection. If the client specify expected
|
* @param serverKey optional key to authenticate the connection. If the client specify expected
|
||||||
* server key it should match of connection will not be established.
|
* server key it should match of connection will not be established.
|
||||||
@ -45,8 +44,8 @@ fun <S> Application.setupWebsocketServer(
|
|||||||
createSession: () -> S,
|
createSession: () -> S,
|
||||||
) {
|
) {
|
||||||
install(WebSockets) {
|
install(WebSockets) {
|
||||||
pingPeriod = Duration.ofSeconds(15)
|
pingPeriod = 60.seconds //Duration.ofSeconds(15)
|
||||||
timeout = Duration.ofSeconds(15)
|
timeout = 45.seconds
|
||||||
maxFrameSize = Long.MAX_VALUE
|
maxFrameSize = Long.MAX_VALUE
|
||||||
masking = false
|
masking = false
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.sergeych.kiloparsec
|
|||||||
import assertThrows
|
import assertThrows
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.crypto2.initCrypto
|
import net.sergeych.crypto2.initCrypto
|
||||||
@ -10,6 +11,7 @@ import net.sergeych.kiloparsec.adapter.setupWebsocketServer
|
|||||||
import net.sergeych.kiloparsec.adapter.websocketClient
|
import net.sergeych.kiloparsec.adapter.websocketClient
|
||||||
import net.sergeych.mp_logger.Log
|
import net.sergeych.mp_logger.Log
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import kotlin.random.Random
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
@ -50,11 +52,12 @@ class ClientTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = {
|
val port = Random.nextInt(8080,9090)
|
||||||
|
val ns = embeddedServer(Netty, port = port, host = "0.0.0.0", module = {
|
||||||
setupWebsocketServer(serverInterface) { Session() }
|
setupWebsocketServer(serverInterface) { Session() }
|
||||||
}).start(wait = false)
|
}).start(wait = false)
|
||||||
|
|
||||||
val client = websocketClient<Unit>("ws://localhost:8080/kp")
|
val client = websocketClient<Unit>("ws://localhost:$port/kp")
|
||||||
val states = mutableListOf<Boolean>()
|
val states = mutableListOf<Boolean>()
|
||||||
val collector = launch {
|
val collector = launch {
|
||||||
client.connectedStateFlow.collect {
|
client.connectedStateFlow.collect {
|
||||||
@ -75,6 +78,9 @@ class ClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connection should now be closed
|
// connection should now be closed
|
||||||
|
// the problem is: it needs some unspecified time to close
|
||||||
|
// as it is async process.
|
||||||
|
delay(100)
|
||||||
assertFalse { client.connectedStateFlow.value }
|
assertFalse { client.connectedStateFlow.value }
|
||||||
|
|
||||||
// this should be run on automatically reopen connection
|
// this should be run on automatically reopen connection
|
||||||
|
@ -3,6 +3,7 @@ package net.sergeych.kiloparsec.adapter
|
|||||||
import io.ktor.network.selector.*
|
import io.ktor.network.selector.*
|
||||||
import io.ktor.network.sockets.*
|
import io.ktor.network.sockets.*
|
||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
|
import io.ktor.utils.io.writeByte
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
@ -12,14 +13,10 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import net.sergeych.kiloparsec.AsyncVarint
|
import net.sergeych.kiloparsec.*
|
||||||
import net.sergeych.kiloparsec.KiloClient
|
|
||||||
import net.sergeych.kiloparsec.KiloServer
|
|
||||||
import net.sergeych.kiloparsec.LocalInterface
|
|
||||||
import net.sergeych.mp_logger.*
|
import net.sergeych.mp_logger.*
|
||||||
import net.sergeych.mp_tools.globalLaunch
|
import net.sergeych.mp_tools.globalLaunch
|
||||||
import net.sergeych.tools.AtomicCounter
|
import net.sergeych.tools.AtomicCounter
|
||||||
import net.sergeych.tools.AtomicValue
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
private val logCounter = AtomicCounter(0)
|
private val logCounter = AtomicCounter(0)
|
||||||
@ -33,10 +30,10 @@ internal val PING_INACTIVITY_TIME = 30.seconds
|
|||||||
* Listen for incoming TCP/IP connections on all local interfaces and the specified [port]
|
* Listen for incoming TCP/IP connections on all local interfaces and the specified [port]
|
||||||
* anc create flow of [InetTransportDevice] suitable for [KiloClient].
|
* anc create flow of [InetTransportDevice] suitable for [KiloClient].
|
||||||
*/
|
*/
|
||||||
fun acceptTcpDevice(port: Int,localInterface: String = "0.0.0.0"): Flow<InetTransportDevice> {
|
fun acceptTcpDevice(port: Int, localInterface: String = "0.0.0.0"): Flow<InetTransportDevice> {
|
||||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||||
val serverSocket = aSocket(selectorManager).tcp().bind(localInterface, port)
|
|
||||||
return flow {
|
return flow {
|
||||||
|
val serverSocket = aSocket(selectorManager).tcp().bind(localInterface, port)
|
||||||
while (true) {
|
while (true) {
|
||||||
serverSocket.accept().let { sock ->
|
serverSocket.accept().let { sock ->
|
||||||
emit(inetTransportDevice(sock, "srv"))
|
emit(inetTransportDevice(sock, "srv"))
|
||||||
@ -74,7 +71,7 @@ private fun inetTransportDevice(
|
|||||||
val outputBlocks = Channel<UByteArray>(4096)
|
val outputBlocks = Channel<UByteArray>(4096)
|
||||||
|
|
||||||
val log = LogTag("TCPT${logCounter.incrementAndGet()}:$suffix:$networkAddress")
|
val log = LogTag("TCPT${logCounter.incrementAndGet()}:$suffix:$networkAddress")
|
||||||
val job = AtomicValue<Job?>(null)
|
val job = AtomicAsyncValue<Job?>(null)
|
||||||
|
|
||||||
val sockOutput = sock.openWriteChannel()
|
val sockOutput = sock.openWriteChannel()
|
||||||
val sockInput = runCatching { sock.openReadChannel() }.getOrElse {
|
val sockInput = runCatching { sock.openReadChannel() }.getOrElse {
|
||||||
@ -82,16 +79,16 @@ private fun inetTransportDevice(
|
|||||||
throw IllegalStateException("failed to open read channel")
|
throw IllegalStateException("failed to open read channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
suspend fun stop() {
|
||||||
job.mutate {
|
job.mutate {
|
||||||
if ( it != null ) {
|
if (it != null) {
|
||||||
log.debug { "stopping" }
|
log.debug { "stopping" }
|
||||||
runCatching { inputBlocks.close() }
|
runCatching { inputBlocks.close() }
|
||||||
runCatching { outputBlocks.close() }
|
runCatching { outputBlocks.close() }
|
||||||
// The problem: on mac platofrms closing the socket does not close its input
|
// The problem: on mac platofrms closing the socket does not close its input
|
||||||
// and output channels!
|
// and output channels!
|
||||||
runCatching { sockInput.cancel() }
|
runCatching { sockInput.cancel() }
|
||||||
runCatching { sockOutput.close() }
|
runCatching { sockOutput.flushAndClose() }
|
||||||
if (!sock.isClosed)
|
if (!sock.isClosed)
|
||||||
runCatching {
|
runCatching {
|
||||||
log.debug { "closing socket by stop" }
|
log.debug { "closing socket by stop" }
|
||||||
@ -108,46 +105,47 @@ private fun inetTransportDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lastActiveAt = Clock.System.now()
|
var lastActiveAt = Clock.System.now()
|
||||||
job.value = globalLaunch {
|
globalLaunch {
|
||||||
launch {
|
job.reset(globalLaunch {
|
||||||
|
launch {
|
||||||
|
|
||||||
log.debug { "opening read channel" }
|
log.debug { "opening read channel" }
|
||||||
|
|
||||||
while (isActive && sock.isActive) {
|
while (isActive && sock.isActive) {
|
||||||
try {
|
try {
|
||||||
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
val size = AsyncVarint.decodeUnsigned(sockInput).toInt()
|
||||||
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
if (size > MAX_TCP_BLOCK_SIZE) // 16M is a max command block
|
||||||
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
throw ProtocolException("Illegal block size: $size should be < $MAX_TCP_BLOCK_SIZE")
|
||||||
val data = ByteArray(size)
|
val data = ByteArray(size)
|
||||||
if (size == 0) {
|
if (size == 0) {
|
||||||
log.debug { "ping received" }
|
log.debug { "ping received" }
|
||||||
lastActiveAt = Clock.System.now()
|
lastActiveAt = Clock.System.now()
|
||||||
} else {
|
} else {
|
||||||
sockInput.readFully(data, 0, size)
|
sockInput.readFully(data, 0, size)
|
||||||
inputBlocks.send(data.toUByteArray())
|
inputBlocks.send(data.toUByteArray())
|
||||||
|
}
|
||||||
|
} catch (e: ClosedReceiveChannelException) {
|
||||||
|
log.error { "closed receive channel " }
|
||||||
|
stop()
|
||||||
|
break
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
log.error { "cancellation exception " }
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.exception { "unexpected exception in TCP socket read" to e }
|
||||||
|
stop()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} catch (e: ClosedReceiveChannelException) {
|
|
||||||
log.error { "closed receive channel " }
|
|
||||||
stop()
|
|
||||||
break
|
|
||||||
} catch (_: CancellationException) {
|
|
||||||
log.error { "cancellation exception " }
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.exception { "unexpected exception in TCP socket read" to e }
|
|
||||||
stop()
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val outAccess = Mutex()
|
val outAccess = Mutex()
|
||||||
var lastSentAt = Clock.System.now()
|
var lastSentAt = Clock.System.now()
|
||||||
launch {
|
launch {
|
||||||
while (isActive && sock.isActive) {
|
while (isActive && sock.isActive) {
|
||||||
delay(500)
|
delay(500)
|
||||||
val activityTime = if(lastSentAt > lastActiveAt) lastSentAt else lastActiveAt
|
val activityTime = if (lastSentAt > lastActiveAt) lastSentAt else lastActiveAt
|
||||||
if (Clock.System.now() - activityTime > PING_INACTIVITY_TIME) {
|
if (Clock.System.now() - activityTime > PING_INACTIVITY_TIME) {
|
||||||
log.debug { "pinging for inactivity" }
|
log.debug { "pinging for inactivity" }
|
||||||
val repeat = outAccess.withLock {
|
val repeat = outAccess.withLock {
|
||||||
|
@ -87,7 +87,7 @@ fun acceptUdpDevice(
|
|||||||
* the module automatically issues pings on inactivity when there is no data often enough
|
* the module automatically issues pings on inactivity when there is no data often enough
|
||||||
* to maintain the connection open.
|
* to maintain the connection open.
|
||||||
*/
|
*/
|
||||||
fun connectUdpDevice(
|
suspend fun connectUdpDevice(
|
||||||
hostPort: String,
|
hostPort: String,
|
||||||
maxInactivityTimeout: Duration = 2.minutes,
|
maxInactivityTimeout: Duration = 2.minutes,
|
||||||
) = connectUdpDevice(hostPort.toNetworkAddress(), maxInactivityTimeout)
|
) = connectUdpDevice(hostPort.toNetworkAddress(), maxInactivityTimeout)
|
||||||
@ -107,16 +107,16 @@ fun connectUdpDevice(
|
|||||||
* the module automatically issues pings on inactivity when there is no data often enough
|
* the module automatically issues pings on inactivity when there is no data often enough
|
||||||
* to maintain the connection open.
|
* to maintain the connection open.
|
||||||
*/
|
*/
|
||||||
fun connectUdpDevice(
|
suspend fun connectUdpDevice(
|
||||||
addr: NetworkAddress,
|
addr: NetworkAddress,
|
||||||
maxInactivityTimeout: Duration = 2.minutes,
|
maxInactivityTimeout: Duration = 2.minutes,
|
||||||
): InetTransportDevice {
|
): InetTransportDevice {
|
||||||
val selectorManager = SelectorManager(Dispatchers.IO)
|
val selectorManager = SelectorManager(Dispatchers.IO)
|
||||||
val remoteAddress = InetSocketAddress(addr.host, addr.port)
|
val remoteAddress = InetSocketAddress(addr.host, addr.port)
|
||||||
val socket = aSocket(selectorManager).udp().connect(remoteAddress)
|
|
||||||
|
|
||||||
val done = CompletableDeferred<Unit>()
|
val done = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
val socket = aSocket(selectorManager).udp().connect(remoteAddress)
|
||||||
val transport = UdpSocketTransport(object : UdpConnector {
|
val transport = UdpSocketTransport(object : UdpConnector {
|
||||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||||
socket.send(block.toDatagram(remoteAddress))
|
socket.send(block.toDatagram(remoteAddress))
|
||||||
|
@ -14,6 +14,7 @@ import net.sergeych.mp_logger.LogTag
|
|||||||
import net.sergeych.mp_logger.Loggable
|
import net.sergeych.mp_logger.Loggable
|
||||||
import net.sergeych.mp_logger.debug
|
import net.sergeych.mp_logger.debug
|
||||||
import net.sergeych.mp_logger.exception
|
import net.sergeych.mp_logger.exception
|
||||||
|
import net.sergeych.mp_tools.globalDefer
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
@ -51,7 +52,9 @@ class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivity
|
|||||||
private val access = Mutex()
|
private val access = Mutex()
|
||||||
|
|
||||||
private val selectorManager = SelectorManager(Dispatchers.IO)
|
private val selectorManager = SelectorManager(Dispatchers.IO)
|
||||||
private val serverSocket = aSocket(selectorManager).udp().bind(InetSocketAddress(localInterface, port))
|
private val serverSocket = globalDefer {
|
||||||
|
aSocket(selectorManager).udp().bind(InetSocketAddress(localInterface, port))
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun disconnectClient(address: SocketAddress) {
|
override suspend fun disconnectClient(address: SocketAddress) {
|
||||||
access.withLock { sessions.remove(address) }
|
access.withLock { sessions.remove(address) }
|
||||||
@ -65,7 +68,7 @@ class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivity
|
|||||||
flow {
|
flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
val datagram = serverSocket.receive()
|
val datagram = serverSocket.await().receive()
|
||||||
val block = UdpBlock.decode(datagram)
|
val block = UdpBlock.decode(datagram)
|
||||||
val remoteAddress = datagram.address
|
val remoteAddress = datagram.address
|
||||||
|
|
||||||
@ -97,10 +100,10 @@ class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
override suspend fun sendBlock(block: UdpBlock, toAddress: SocketAddress) {
|
||||||
serverSocket.send(block.toDatagram(toAddress))
|
serverSocket.await().send(block.toDatagram(toAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
val isClosed: Boolean get() = serverSocket.isClosed
|
suspend fun isClosed(): Boolean = serverSocket.await().isClosed
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the UDP server. Calling it will cause:
|
* Close the UDP server. Calling it will cause:
|
||||||
@ -113,8 +116,8 @@ class UdpServer(val port: Int, localInterface: String = "0.0.0.0", maxInactivity
|
|||||||
*/
|
*/
|
||||||
suspend fun close() {
|
suspend fun close() {
|
||||||
access.withLock {
|
access.withLock {
|
||||||
if (!isClosed) {
|
if (!isClosed()) {
|
||||||
runCatching { serverSocket.close() }
|
runCatching { serverSocket.await().close() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (sessions.isNotEmpty()) {
|
while (sessions.isNotEmpty()) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user