All Downloads are FREE. Search and download functionalities are using the official Maven repository.

appleMain.com.appstractive.dnssd.Discovery.apple.kt Maven / Gradle / Ivy

package com.appstractive.dnssd

import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.allocArray
import kotlinx.cinterop.convert
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.pointed
import kotlinx.cinterop.ptr
import kotlinx.cinterop.reinterpret
import kotlinx.cinterop.toKString
import kotlinx.cinterop.usePinned
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.withContext
import platform.Foundation.NSData
import platform.Foundation.NSNetService
import platform.Foundation.NSNetServiceBrowser
import platform.Foundation.NSNetServiceBrowserDelegateProtocol
import platform.Foundation.NSNetServiceDelegateProtocol
import platform.darwin.NSObject
import platform.darwin.inet_ntop
import platform.posix.AF_INET
import platform.posix.AF_INET6
import platform.posix.INET6_ADDRSTRLEN
import platform.posix.INET_ADDRSTRLEN
import platform.posix.memcpy
import platform.posix.sa_family_t
import platform.posix.sockaddr_in
import platform.posix.sockaddr_in6

internal val String.qualified
  get() = if (this.endsWith(".")) this else "$this."

actual fun discoverServices(type: String): Flow = callbackFlow {
  val serviceBrowser = withContext(Dispatchers.Main) { NSNetServiceBrowser() }
  serviceBrowser.includesPeerToPeer = true

  fun resolve(service: NSNetService) {
    service.delegate =
        object : NSObject(), NSNetServiceDelegateProtocol {
          override fun netServiceDidResolveAddress(sender: NSNetService) {
            println("netServiceDidResolveAddress")
            service.delegate = null
            trySend(
                DiscoveryEvent.Resolved(
                    service = sender.toCommon(),
                ) {
                  resolve(service)
                },
            )
          }
        }
    service.resolve()
  }

  serviceBrowser.delegate =
      object : NSObject(), NSNetServiceBrowserDelegateProtocol {

        override fun netServiceBrowser(browser: NSNetServiceBrowser, didNotSearch: Map) {
          println(
              "NSNetServiceBrowser search error ${didNotSearch.entries.joinToString { error -> "${error.key}:${error.value}" }}",
          )
        }

        override fun netServiceBrowserWillSearch(browser: NSNetServiceBrowser) {
          println("netServiceBrowserWillSearch")
        }

        override fun netServiceBrowserDidStopSearch(browser: NSNetServiceBrowser) {
          println("netServiceBrowserDidStopSearch")
        }

        @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
        @ObjCSignatureOverride
        override fun netServiceBrowser(
            browser: NSNetServiceBrowser,
            didFindDomain: String,
            moreComing: Boolean
        ) {
          println("didFindDomain: $didFindDomain")
        }

        @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
        @ObjCSignatureOverride
        override fun netServiceBrowser(
            browser: NSNetServiceBrowser,
            didRemoveDomain: String,
            moreComing: Boolean
        ) {
          println("didRemoveDomain: $didRemoveDomain")
        }

        @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
        @ObjCSignatureOverride
        override fun netServiceBrowser(
            browser: NSNetServiceBrowser,
            didFindService: NSNetService,
            moreComing: Boolean
        ) {
          println("didFindService")
          trySend(
              DiscoveryEvent.Discovered(
                  service = didFindService.toCommon(),
              ) {
                resolve(didFindService)
              },
          )
        }

        @Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
        @ObjCSignatureOverride
        override fun netServiceBrowser(
            browser: NSNetServiceBrowser,
            didRemoveService: NSNetService,
            moreComing: Boolean
        ) {
          println("didRemoveService")
          trySend(
              DiscoveryEvent.Removed(
                  service = didRemoveService.toCommon(),
              ),
          )
        }
      }

  // serviceBrowser.searchForRegistrationDomains()
  println("Find ${type.qualified}")
  withContext(Dispatchers.Main) {
    serviceBrowser.searchForServicesOfType(type = type.qualified, inDomain = "local.")
  }

  awaitClose { serviceBrowser.stop() }
}

private fun NSNetService.toCommon(): DiscoveredService =
    DiscoveredService(
        name = name,
        addresses =
            addresses?.filterIsInstance()?.mapNotNull { it.toIpString() } ?: emptyList(),
        host = hostName ?: "",
        type = type,
        port = port.toInt(),
        txt =
            buildMap {
              TXTRecordData()?.let {
                val data: Map? = NSNetService.dictionaryFromTXTRecordData(it)

                data?.forEach { (key, value) ->
                  println("Service attributes: $key=$value")
                  if (key is String && value is NSData?) {
                    put(key, value?.toByteArray())
                  }
                }
              }
            },
    )

@OptIn(ExperimentalForeignApi::class)
private fun NSData.toIpString(): String? = memScoped {
  return bytes?.getPointer(this)?.let {
    val serverAddr = it.reinterpret()

    val addr =
        when (serverAddr.pointed.sin_family) {
          AF_INET.convert() -> serverAddr.pointed.sin_addr
          AF_INET6.convert() -> it.reinterpret().pointed.sin6_addr
          else -> return null
        }

    val len = maxOf(INET_ADDRSTRLEN, INET6_ADDRSTRLEN)
    val dst = allocArray(len)
    inet_ntop(serverAddr.pointed.sin_family.convert(), addr.ptr, dst, len.convert())?.toKString()
  }
}

@OptIn(ExperimentalForeignApi::class)
private fun NSData.toByteArray(): ByteArray? {
  if (length.toInt() == 0) {
    return null
  }

  return ByteArray([email protected]()).apply {
    usePinned { memcpy(it.addressOf(0), [email protected], [email protected]) }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy