From aa74d8129a6c85d6c206886acf3de499841ca6ff Mon Sep 17 00:00:00 2001 From: Jorge Antonio Diaz-Benito Soriano Date: Tue, 23 Jan 2024 21:45:02 +0100 Subject: [PATCH] Implement UDP exchange in Kotlin/Native --- .idea/artifacts/library_jvm.xml | 4 +- ...amples_multiplatform_kotlin_shared_jvm.xml | 4 +- .../networktime/internal/HostNameResolver.kt | 24 +- .../internal/NTPUDPSocketOperations.kt | 218 +++++++----------- .../com/tidal/networktime/SNTPClient.kt | 10 +- .../networktime/internal/HostNameResolver.kt | 3 +- .../networktime/internal/NTPExchanger.kt | 8 +- .../internal/NTPUDPSocketOperations.kt | 12 +- .../networktime/internal/SNTPClientImpl.kt | 8 +- .../networktime/internal/SyncSingular.kt | 8 +- .../networktime/internal/HostNameResolver.kt | 4 +- .../internal/NTPUDPSocketOperations.kt | 32 +-- .../UserInterfaceState.xcuserstate | Bin 30768 -> 33812 bytes 13 files changed, 127 insertions(+), 208 deletions(-) diff --git a/.idea/artifacts/library_jvm.xml b/.idea/artifacts/library_jvm.xml index 757db4f6..3c630d1e 100644 --- a/.idea/artifacts/library_jvm.xml +++ b/.idea/artifacts/library_jvm.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/library/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/samples_multiplatform_kotlin_shared_jvm.xml b/.idea/artifacts/samples_multiplatform_kotlin_shared_jvm.xml index 036e9d10..5f695921 100644 --- a/.idea/artifacts/samples_multiplatform_kotlin_shared_jvm.xml +++ b/.idea/artifacts/samples_multiplatform_kotlin_shared_jvm.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/samples/multiplatform-kotlin/shared/build/libs - - - + \ No newline at end of file diff --git a/library/src/appleMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt b/library/src/appleMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt index 6951eb24..585a7322 100644 --- a/library/src/appleMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt +++ b/library/src/appleMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt @@ -6,6 +6,7 @@ import kotlinx.cinterop.ByteVar import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.alloc import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped import kotlinx.cinterop.pointed import kotlinx.cinterop.ptr @@ -51,8 +52,8 @@ internal actual class HostNameResolver { timeout: Duration, includeINET: Boolean, includeINET6: Boolean, - ): Iterable> { - var ret: Iterable>? = emptySet() + ): Iterable { + var ret: Iterable? = emptySet() try { ret = withTimeoutOrNull(timeout) { invokeInternal(hostName, includeINET, includeINET6) } } finally { @@ -70,7 +71,7 @@ internal actual class HostNameResolver { hostName: String, includeINET: Boolean, includeINET6: Boolean, - ): Iterable> { + ): Iterable { hostReference = CFBridgingRetain(hostName as NSString) cfHost = CFHostCreateWithName(kCFAllocatorDefault, hostReference as CFStringRef) CFHostStartInfoResolution(cfHost, kCFHostAddresses, null) @@ -82,11 +83,11 @@ internal actual class HostNameResolver { addresses.takeIf { hasResolved.value } addresses ?: return emptySet() val count = CFArrayGetCount(addresses) - val ret = mutableSetOf>() + val ret = mutableSetOf() (0 until count).forEach { val socketAddressData = CFArrayGetValueAtIndex(addresses, it) as CFDataRef val sockAddr = CFDataGetBytePtr(socketAddressData)!!.reinterpret().pointed - val addrPrettyToProtocolFamily = when (sockAddr.sa_family.toInt()) { + val addrPretty = when (sockAddr.sa_family.toInt()) { AF_INET -> { if (includeINET) { val buffer = allocArray(INET_ADDRSTRLEN) @@ -94,9 +95,9 @@ internal actual class HostNameResolver { AF_INET, sockAddr.reinterpret().sin_addr.readValue(), buffer, - INET_ADDRSTRLEN.toUInt(), + INET_ADDRSTRLEN.convert(), ) - buffer.toKString() to ProtocolFamily.INET + buffer.toKString() } else { null } @@ -109,9 +110,9 @@ internal actual class HostNameResolver { AF_INET6, sockAddr.reinterpret().sin6_addr.readValue(), buffer, - INET6_ADDRSTRLEN.toUInt(), + INET6_ADDRSTRLEN.convert(), ) - buffer.toKString() to ProtocolFamily.INET6 + buffer.toKString() } else { null } @@ -121,8 +122,8 @@ internal actual class HostNameResolver { null } } - if (addrPrettyToProtocolFamily != null) { - ret.add(addrPrettyToProtocolFamily) + if (addrPretty != null) { + ret.add(addrPretty) } } ret @@ -130,6 +131,7 @@ internal actual class HostNameResolver { } private fun clear() { + cfHost?.let { CFRelease(it) } cfHost = null hostReference?.let { CFRelease(it) } hostReference = null diff --git a/library/src/appleMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt b/library/src/appleMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt index ce2f74b8..b34f6c76 100644 --- a/library/src/appleMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt +++ b/library/src/appleMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt @@ -1,158 +1,106 @@ package com.tidal.networktime.internal -import com.tidal.networktime.ProtocolFamily +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.StableRef -import kotlinx.cinterop.alloc -import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert import kotlinx.cinterop.memScoped -import kotlinx.cinterop.pointed -import kotlinx.cinterop.ptr import kotlinx.cinterop.refTo import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.sizeOf -import kotlinx.cinterop.staticCFunction -import kotlinx.cinterop.toCPointer -import kotlinx.cinterop.toLong -import kotlinx.cinterop.value -import kotlinx.coroutines.delay +import kotlinx.cinterop.set +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.withTimeout -import platform.CoreFoundation.CFDataCreate -import platform.CoreFoundation.CFDataGetBytes -import platform.CoreFoundation.CFDataGetLength -import platform.CoreFoundation.CFDataRefVar -import platform.CoreFoundation.CFRangeMake -import platform.CoreFoundation.CFSocketCallBack -import platform.CoreFoundation.CFSocketConnectToAddress -import platform.CoreFoundation.CFSocketContext -import platform.CoreFoundation.CFSocketCreate -import platform.CoreFoundation.CFSocketInvalidate -import platform.CoreFoundation.CFSocketRef -import platform.CoreFoundation.CFSocketSendData -import platform.CoreFoundation.kCFAllocatorDefault -import platform.CoreFoundation.kCFSocketDataCallBack -import platform.darwin.inet_aton -import platform.darwin.inet_pton -import platform.posix.AF_INET -import platform.posix.AF_INET6 -import platform.posix.IPPROTO_UDP -import platform.posix.PF_INET -import platform.posix.PF_INET6 -import platform.posix.SIGPIPE -import platform.posix.SIG_IGN -import platform.posix.SOCK_DGRAM -import platform.posix.signal -import platform.posix.sockaddr_in -import platform.posix.sockaddr_in6 +import platform.Network.NW_CONNECTION_FINAL_MESSAGE_CONTEXT +import platform.Network.NW_PARAMETERS_DEFAULT_CONFIGURATION +import platform.Network.NW_PARAMETERS_DISABLE_PROTOCOL +import platform.Network.nw_connection_create +import platform.Network.nw_connection_force_cancel +import platform.Network.nw_connection_receive +import platform.Network.nw_connection_send +import platform.Network.nw_connection_set_queue +import platform.Network.nw_connection_set_state_changed_handler +import platform.Network.nw_connection_start +import platform.Network.nw_connection_state_cancelled +import platform.Network.nw_connection_state_failed +import platform.Network.nw_connection_state_ready +import platform.Network.nw_connection_state_t +import platform.Network.nw_connection_t +import platform.Network.nw_endpoint_create_host +import platform.Network.nw_error_t +import platform.Network.nw_parameters_create_secure_udp +import platform.darwin._dispatch_data_destructor_free +import platform.darwin.dispatch_data_apply +import platform.darwin.dispatch_data_create +import platform.darwin.dispatch_data_t +import platform.darwin.dispatch_get_current_queue +import platform.posix.memcpy +import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds -import kotlin.time.DurationUnit -@OptIn(ExperimentalForeignApi::class) @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") internal actual class NTPUDPSocketOperations { - private var cfSocket: CFSocketRef? = null - private var userDataRef: StableRef? = null + private var connection: nw_connection_t = null - actual suspend fun prepareSocket( - address: String, - protocolFamily: ProtocolFamily, - portNumber: Int, - connectTimeout: Duration, - ) { - userDataRef = StableRef.create(UserData()) - val callback: CFSocketCallBack = staticCFunction { _, callbackType, _, data, info -> - val userData = info!!.asStableRef().get() - if (callbackType != kCFSocketDataCallBack) { - return@staticCFunction - } - val reinterpretedData = data!!.reinterpret().pointed.value - val length = CFDataGetLength(reinterpretedData) - val range = CFRangeMake(0, length) - val bridgeBuffer = UByteArray(length.toInt()) - CFDataGetBytes(reinterpretedData, range, bridgeBuffer.refTo(0)) - userData.apply { - buffer = bridgeBuffer.toByteArray() - exchangeCompleted = true + actual suspend fun prepare(address: String, portNumber: Int, connectTimeout: Duration) { + val parameters = nw_parameters_create_secure_udp( + NW_PARAMETERS_DISABLE_PROTOCOL, + NW_PARAMETERS_DEFAULT_CONFIGURATION + ) + val endpoint = nw_endpoint_create_host(address, portNumber.toString()) + connection = nw_connection_create(endpoint, parameters) + nw_connection_set_queue(connection, dispatch_get_current_queue()) + val connectionStateDeferred = CompletableDeferred() + nw_connection_set_state_changed_handler(connection) { state: nw_connection_state_t, _ -> + when (state) { + nw_connection_state_ready, nw_connection_state_failed, nw_connection_state_cancelled -> + connectionStateDeferred.complete(state) } } - signal(SIGPIPE, SIG_IGN) - cfSocket = memScoped { - val socketContext = alloc { - version = 0 - info = userDataRef!!.asCPointer() - retain = null - release = null - copyDescription = null - } - val socket = CFSocketCreate( - kCFAllocatorDefault, - when (protocolFamily) { - ProtocolFamily.INET -> PF_INET - ProtocolFamily.INET6 -> PF_INET6 - }, - SOCK_DGRAM, - IPPROTO_UDP, - kCFSocketDataCallBack, - callback, - socketContext.ptr, - ) - val addrCFDataRef = when (protocolFamily) { - ProtocolFamily.INET -> alloc { - sin_family = AF_INET.toUByte() - sin_port = portNumber.toUShort() - inet_aton(address, sin_addr.ptr) - }.run { - CFDataCreate(kCFAllocatorDefault, ptr.toLong().toCPointer(), sizeOf()) - } - - ProtocolFamily.INET6 -> { - alloc { - sin6_family = AF_INET6.toUByte() - sin6_port = portNumber.toUShort() - inet_pton(AF_INET6, address, sin6_addr.ptr) - }.run { - CFDataCreate(kCFAllocatorDefault, ptr.toLong().toCPointer(), sizeOf()) - } - } - } - CFSocketConnectToAddress( - socket, - addrCFDataRef, - connectTimeout.toDouble(DurationUnit.MILLISECONDS), - ) - socket + nw_connection_start(connection) + withTimeout(connectTimeout) { + assertEquals(nw_connection_state_ready, connectionStateDeferred.await()) } } - actual suspend fun exchangeInPlace(buffer: ByteArray, readTimeout: Duration) { - val bufferCFDataRef = CFDataCreate( - kCFAllocatorDefault, - buffer.asUByteArray().refTo(0), - buffer.size.toLong(), - ) - CFSocketSendData( - cfSocket, - null, - bufferCFDataRef, - Duration.INFINITE.toDouble(DurationUnit.MILLISECONDS), - ) - withTimeout(readTimeout) { - while (!userDataRef!!.get().exchangeCompleted) { - delay(1.seconds) + @OptIn(ExperimentalForeignApi::class) + actual suspend fun exchange(buffer: ByteArray, readTimeout: Duration) { + val toSendData = memScoped { + val cArray = allocArray(buffer.size) + buffer.forEachIndexed { i, it -> + cArray[i] = it } + cArray + } + nw_connection_send( + connection, + dispatch_data_create(toSendData, buffer.size.convert(), null, _dispatch_data_destructor_free), + NW_CONNECTION_FINAL_MESSAGE_CONTEXT, + true + ) { + assertNull(it) + } + val connectionReceptionDeferred = CompletableDeferred() + nw_connection_receive( + connection, + 1.convert(), + buffer.size.convert() + ) { content: dispatch_data_t, _, _, error: nw_error_t -> + assertNull(error) + connectionReceptionDeferred.complete(content) + } + val receivedData = withTimeout(readTimeout) { + connectionReceptionDeferred.await() + } + dispatch_data_apply(receivedData) { _, _, regionPointer, _ -> + memcpy(buffer.refTo(0), regionPointer!!.reinterpret(), buffer.size.convert()) + false } } - actual fun closeSocket() { - CFSocketInvalidate(cfSocket) - cfSocket = null - userDataRef?.dispose() - userDataRef = null - } - - private class UserData { - var exchangeCompleted = false - var buffer: ByteArray = byteArrayOf() + actual fun tearDown() { + nw_connection_force_cancel(connection) + connection = null } } diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/SNTPClient.kt b/library/src/commonMain/kotlin/com/tidal/networktime/SNTPClient.kt index 6107da81..ca0b6404 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/SNTPClient.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/SNTPClient.kt @@ -1,9 +1,6 @@ package com.tidal.networktime import com.tidal.networktime.internal.SNTPClientImpl -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import okio.Path.Companion.toPath import kotlin.time.Duration @@ -14,7 +11,6 @@ import kotlin.time.Duration.Companion.seconds * [ntpServers] to obtain information about their provided time. * * @param ntpServers Representation of supported unicast NTP sources. - * @param coroutineScope The scope where synchronization will run on. * @param synchronizationInterval The amount of time to wait between a sync finishing and the next * one being started. * @param backupFilePath A path to a file that will be used to save the selected received NTP @@ -23,17 +19,13 @@ import kotlin.time.Duration.Companion.seconds * packet has been received and processed. If not `null` but writing or reading fail when attempted, * program execution will continue as if it had been `null` until the next attempt. */ -class SNTPClient -@OptIn(DelicateCoroutinesApi::class) -constructor( +class SNTPClient( vararg val ntpServers: NTPServer, - val coroutineScope: CoroutineScope = GlobalScope, val synchronizationInterval: Duration = 64.seconds, val backupFilePath: String? = null, ) { private val delegate = SNTPClientImpl( ntpServers, - coroutineScope, backupFilePath?.toPath(), synchronizationInterval, ) diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt b/library/src/commonMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt index acec66cf..f798f5b2 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt @@ -1,6 +1,5 @@ package com.tidal.networktime.internal -import com.tidal.networktime.ProtocolFamily import kotlin.time.Duration @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") @@ -10,5 +9,5 @@ internal expect class HostNameResolver() { timeout: Duration, includeINET: Boolean, includeINET6: Boolean, - ): Iterable> + ): Iterable } diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPExchanger.kt b/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPExchanger.kt index f37940be..9173b30a 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPExchanger.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPExchanger.kt @@ -1,6 +1,5 @@ package com.tidal.networktime.internal -import com.tidal.networktime.ProtocolFamily import kotlin.time.Duration internal class NTPExchanger( @@ -10,25 +9,24 @@ internal class NTPExchanger( ) { suspend operator fun invoke( address: String, - protocolFamily: ProtocolFamily, connectTimeout: Duration, queryReadTimeout: Duration, ntpVersion: UByte, ): NTPExchangeResult? { val ntpUdpSocketOperations = NTPUDPSocketOperations() return try { - ntpUdpSocketOperations.prepareSocket(address, protocolFamily, NTP_PORT_NUMBER, connectTimeout) + ntpUdpSocketOperations.prepare(address, NTP_PORT_NUMBER, connectTimeout) val ntpPacket = NTPPacket(versionNumber = ntpVersion.toInt(), mode = NTP_MODE_CLIENT) val requestTime = referenceClock.referenceEpochTime ntpPacket.transmitEpochTimestamp = EpochTimestamp(requestTime).asNTPTimestamp val buffer = ntpPacketSerializer(ntpPacket) - ntpUdpSocketOperations.exchangeInPlace(buffer, queryReadTimeout) + ntpUdpSocketOperations.exchange(buffer, queryReadTimeout) val returnTime = referenceClock.referenceEpochTime ntpPacketDeserializer(buffer)?.let { NTPExchangeResult(returnTime, it) } } catch (_: Throwable) { null } finally { - ntpUdpSocketOperations.closeSocket() + ntpUdpSocketOperations.tearDown() } } diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt b/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt index d31e8945..49d1df4c 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt @@ -1,18 +1,12 @@ package com.tidal.networktime.internal -import com.tidal.networktime.ProtocolFamily import kotlin.time.Duration @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") internal expect class NTPUDPSocketOperations() { - suspend fun prepareSocket( - address: String, - protocolFamily: ProtocolFamily, - portNumber: Int, - connectTimeout: Duration, - ) + suspend fun prepare(address: String, portNumber: Int, connectTimeout: Duration) - suspend fun exchangeInPlace(buffer: ByteArray, readTimeout: Duration) + suspend fun exchange(buffer: ByteArray, readTimeout: Duration) - fun closeSocket() + fun tearDown() } diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/internal/SNTPClientImpl.kt b/library/src/commonMain/kotlin/com/tidal/networktime/internal/SNTPClientImpl.kt index 9d31a207..a4f9386c 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/internal/SNTPClientImpl.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/internal/SNTPClientImpl.kt @@ -1,15 +1,15 @@ package com.tidal.networktime.internal import com.tidal.networktime.NTPServer -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.IO import okio.Path import kotlin.time.Duration -internal class SNTPClientImpl( +internal class SNTPClientImpl @OptIn(DelicateCoroutinesApi::class) constructor( ntpServers: Array, - coroutineScope: CoroutineScope, backupFilePath: Path?, syncInterval: Duration, private val referenceClock: KotlinXDateTimeSystemClock = KotlinXDateTimeSystemClock(), @@ -23,7 +23,7 @@ internal class SNTPClientImpl( OperationCoordinator( mutableState, synchronizationResultProcessor, - coroutineScope, + GlobalScope, Dispatchers.IO, syncInterval, ntpServers.asIterable(), diff --git a/library/src/commonMain/kotlin/com/tidal/networktime/internal/SyncSingular.kt b/library/src/commonMain/kotlin/com/tidal/networktime/internal/SyncSingular.kt index 302672a8..c8b28991 100644 --- a/library/src/commonMain/kotlin/com/tidal/networktime/internal/SyncSingular.kt +++ b/library/src/commonMain/kotlin/com/tidal/networktime/internal/SyncSingular.kt @@ -3,9 +3,8 @@ package com.tidal.networktime.internal import com.tidal.networktime.NTPServer import com.tidal.networktime.NTPVersion import com.tidal.networktime.ProtocolFamily -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.IO import kotlinx.coroutines.async +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -18,7 +17,7 @@ internal class SyncSingular( ) { suspend operator fun invoke() { val selectedResult = ntpServers.map { - withContext(Dispatchers.IO) { + withContext(currentCoroutineContext()) { async { pickNTPPacketWithShortestRoundTrip(it) } } }.flatMap { @@ -45,11 +44,10 @@ internal class SyncSingular( dnsResolutionTimeout, ProtocolFamily.INET in protocolFamilies, ProtocolFamily.INET6 in protocolFamilies, - ).map { (resolvedName, resolvedProtocolFamily) -> + ).map { resolvedName -> (1..queriesPerResolvedAddress).mapNotNull { val ret = ntpExchanger( resolvedName, - resolvedProtocolFamily, queryConnectTimeout, queryReadTimeout, when (ntpVersion) { diff --git a/library/src/jvmMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt b/library/src/jvmMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt index 8c7c8449..4c52465b 100644 --- a/library/src/jvmMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt +++ b/library/src/jvmMain/kotlin/com/tidal/networktime/internal/HostNameResolver.kt @@ -14,7 +14,7 @@ internal actual class HostNameResolver { timeout: Duration, includeINET: Boolean, includeINET6: Boolean, - ): Iterable> = withTimeoutOrNull(timeout) { + ): Iterable = withTimeoutOrNull(timeout) { InetAddress.getAllByName(hostName) }?.mapNotNull { val protocolFamily = when (it) { @@ -24,7 +24,7 @@ internal actual class HostNameResolver { } when { protocolFamily == ProtocolFamily.INET && includeINET || - protocolFamily == ProtocolFamily.INET6 && includeINET6 -> it.hostAddress to protocolFamily + protocolFamily == ProtocolFamily.INET6 && includeINET6 -> it.hostAddress else -> null } diff --git a/library/src/jvmMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt b/library/src/jvmMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt index 9dfcdcfe..8874155f 100644 --- a/library/src/jvmMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt +++ b/library/src/jvmMain/kotlin/com/tidal/networktime/internal/NTPUDPSocketOperations.kt @@ -1,8 +1,5 @@ package com.tidal.networktime.internal -import com.tidal.networktime.ProtocolFamily -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.net.DatagramPacket import java.net.DatagramSocket @@ -10,30 +7,25 @@ import java.net.InetAddress import kotlin.time.Duration import kotlin.time.DurationUnit -@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "BlockingMethodInNonBlockingContext") internal actual class NTPUDPSocketOperations { - private lateinit var datagramSocket: DatagramSocket + private var datagramSocket: DatagramSocket? = null - actual suspend fun prepareSocket( - address: String, - protocolFamily: ProtocolFamily, - portNumber: Int, - connectTimeout: Duration, - ) = withTimeout(connectTimeout) { + actual suspend fun prepare(address: String, portNumber: Int, connectTimeout: Duration) { datagramSocket = DatagramSocket() - datagramSocket.connect(InetAddress.getByName(address), portNumber) + withTimeout(connectTimeout) { + datagramSocket!!.connect(InetAddress.getByName(address), portNumber) + } } - actual suspend fun exchangeInPlace(buffer: ByteArray, readTimeout: Duration) { + actual suspend fun exchange(buffer: ByteArray, readTimeout: Duration) { val exchangePacket = DatagramPacket(buffer, buffer.size) - withContext(Dispatchers.IO) { - datagramSocket.send(exchangePacket) - datagramSocket.soTimeout = readTimeout.toInt(DurationUnit.MILLISECONDS) - datagramSocket.receive(exchangePacket) - } + datagramSocket!!.send(exchangePacket) + datagramSocket!!.soTimeout = readTimeout.toInt(DurationUnit.MILLISECONDS) + datagramSocket!!.receive(exchangePacket) } - actual fun closeSocket() { - datagramSocket.close() + actual fun tearDown() { + datagramSocket?.close() } } diff --git a/samples/multiplatform-kotlin/iOS.xcodeproj/project.xcworkspace/xcuserdata/jantonio.xcuserdatad/UserInterfaceState.xcuserstate b/samples/multiplatform-kotlin/iOS.xcodeproj/project.xcworkspace/xcuserdata/jantonio.xcuserdatad/UserInterfaceState.xcuserstate index 1d9413c7cf0df7fdba14e2e3e274bc5c3caf8827..178ec82d37a7f0748621d7f7f4024fc561e7f742 100644 GIT binary patch delta 17926 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 Z%?qb~t(TM#wp{iQ{A>N+w%`4z{{w#TSn~h? delta 16433 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 DA5ztK