@dawidzawada/bonjour-zeroconf 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +32 -20
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+Listeners.kt +4 -5
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +18 -16
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/Loggy.kt +16 -0
- package/ios/BonjourZeroconf+AddressResolver.swift +32 -32
- package/ios/BonjourZeroconf+Listeners.swift +5 -11
- package/ios/BonjourZeroconf.swift +66 -71
- package/ios/ListenerStore.swift +43 -0
- package/ios/Utils/Loggy.swift +21 -4
- package/lib/module/BonjourScanner.js +34 -0
- package/lib/module/BonjourScanner.js.map +1 -0
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/permissions.ios.js.map +1 -1
- package/lib/typescript/src/BonjourScanner.d.ts +20 -0
- package/lib/typescript/src/BonjourScanner.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/permissions.ios.d.ts +1 -1
- package/lib/typescript/src/permissions.ios.d.ts.map +1 -1
- package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +1 -0
- package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +11 -2
- package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +6 -0
- package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +15 -15
- package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +8 -1
- package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +24 -0
- package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +3 -1
- package/package.json +1 -1
- package/src/BonjourScanner.ts +59 -0
- package/src/index.ts +4 -4
- package/src/permissions.ios.ts +4 -1
- package/src/specs/BonjourZeroconf.nitro.ts +1 -0
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Discover devices and services on your local network using native Bonjour (iOS) a
|
|
|
17
17
|
- 📡 **Cross-platform** – iOS (Bonjour) and Android (NSD)
|
|
18
18
|
- 📱 **Managing iOS permissions** - no need for extra libraries or custom code, just use `requestLocalNetworkPermission` or `useLocalNetworkPermission` before scanning!
|
|
19
19
|
- 🔄 **Real-time updates** – listen to scan results, state changes, and errors
|
|
20
|
+
- 🔭 **Multiple scanners** – run several independent scanners simultaneously for different service types
|
|
20
21
|
- 🧩 **Expo compatible** - (config plugin coming soon)
|
|
21
22
|
|
|
22
23
|
## 📦 Installation
|
|
@@ -125,10 +126,33 @@ function App() {
|
|
|
125
126
|
|
|
126
127
|
---
|
|
127
128
|
|
|
129
|
+
## 🔭 Multiple Scanners
|
|
130
|
+
|
|
131
|
+
For convenience, the library exports a `Scanner` singleton that covers most use cases. Each scanner can only scan one service type at a time, so if you need to discover multiple service types simultaneously, use the `BonjourScanner` class to create as many independent scanners as you need.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { BonjourScanner } from '@dawidzawada/bonjour-zeroconf';
|
|
135
|
+
|
|
136
|
+
const printerScanner = new BonjourScanner({ id: 'printers' });
|
|
137
|
+
const httpScanner = new BonjourScanner({ id: 'http' });
|
|
138
|
+
|
|
139
|
+
// Both run in parallel, scanning different service types
|
|
140
|
+
printerScanner.scan('_printer._tcp', 'local');
|
|
141
|
+
httpScanner.scan('_http._tcp', 'local');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The optional `id` is appended to log messages, making it easy to distinguish between scanners during debugging.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
128
148
|
## 📖 API Reference
|
|
129
149
|
|
|
130
150
|
### **Scanner**
|
|
131
151
|
|
|
152
|
+
A pre-created `BonjourScanner` singleton exported for convenience. Use it when you only need to scan one service type at a time.
|
|
153
|
+
|
|
154
|
+
### **BonjourScanner**
|
|
155
|
+
|
|
132
156
|
#### `scan(type: string, domain: string, options?: ScanOptions)`
|
|
133
157
|
|
|
134
158
|
Start scanning for services.
|
|
@@ -273,6 +297,7 @@ interface ScanOptions {
|
|
|
273
297
|
enum BonjourFail {
|
|
274
298
|
DISCOVERY_FAILED = 'DISCOVERY_FAILED',
|
|
275
299
|
RESOLVE_FAILED = 'RESOLVE_FAILED',
|
|
300
|
+
EXTRACTION_FAILED = 'EXTRACTION_FAILED',
|
|
276
301
|
}
|
|
277
302
|
```
|
|
278
303
|
|
|
@@ -2,12 +2,14 @@ package com.margelo.nitro.dawidzawada.bonjourzeroconf
|
|
|
2
2
|
|
|
3
3
|
import android.net.nsd.NsdManager
|
|
4
4
|
import android.net.nsd.NsdServiceInfo
|
|
5
|
-
import android.util.Log
|
|
6
5
|
import androidx.annotation.RequiresApi
|
|
7
|
-
import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.
|
|
6
|
+
import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.legacyResolveMutex
|
|
7
|
+
import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.loggy
|
|
8
8
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
9
9
|
import kotlinx.coroutines.sync.withLock
|
|
10
10
|
import kotlinx.coroutines.withTimeoutOrNull
|
|
11
|
+
import java.net.Inet6Address
|
|
12
|
+
import java.net.NetworkInterface
|
|
11
13
|
import java.util.concurrent.Executors
|
|
12
14
|
|
|
13
15
|
suspend fun BonjourZeroconf.resolveService(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
|
|
@@ -40,19 +42,19 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
|
|
|
40
42
|
try {
|
|
41
43
|
manager.unregisterServiceInfoCallback(this)
|
|
42
44
|
} catch (e: Exception) {
|
|
43
|
-
|
|
45
|
+
loggy.e("Error unregistering", id, e)
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
|
48
|
-
|
|
50
|
+
loggy.e("Registration failed: ${service.serviceName}, error: $errorCode", id)
|
|
49
51
|
notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
|
|
50
52
|
unregisterCallback()
|
|
51
53
|
continuation.resume(null) {}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
|
55
|
-
|
|
57
|
+
loggy.d("Service updated: ${serviceInfo.serviceName}", id)
|
|
56
58
|
unregisterCallback()
|
|
57
59
|
|
|
58
60
|
if (!_isScanning) {
|
|
@@ -64,11 +66,11 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
override fun onServiceLost() {
|
|
67
|
-
|
|
69
|
+
loggy.d("Service lost during resolution: ${service.serviceName}", id)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
override fun onServiceInfoCallbackUnregistered() {
|
|
71
|
-
|
|
73
|
+
loggy.d("Callback unregistered: ${service.serviceName}", id)
|
|
72
74
|
executor.shutdown()
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -80,13 +82,13 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
|
|
|
80
82
|
try {
|
|
81
83
|
manager.unregisterServiceInfoCallback(callback)
|
|
82
84
|
} catch (e: Exception) {
|
|
83
|
-
|
|
85
|
+
loggy.e("Error unregistering on cancellation", id, e)
|
|
84
86
|
executor.shutdown()
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
} catch (e: Exception) {
|
|
88
90
|
notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
|
|
89
|
-
|
|
91
|
+
loggy.e("Exception registering callback", id, e)
|
|
90
92
|
executor.shutdown()
|
|
91
93
|
continuation.resume(null) {}
|
|
92
94
|
}
|
|
@@ -98,11 +100,11 @@ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKe
|
|
|
98
100
|
serviceCache[serviceKey] = scanResult
|
|
99
101
|
notifyScanResultsListeners()
|
|
100
102
|
}
|
|
101
|
-
} ?:
|
|
103
|
+
} ?: loggy.w("Failed to resolve service: $serviceKey", id)
|
|
102
104
|
|
|
103
105
|
} catch (e: Exception) {
|
|
104
106
|
notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
|
|
105
|
-
|
|
107
|
+
loggy.e("Error during service resolution", id, e)
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -114,14 +116,14 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
|
|
|
114
116
|
suspendCancellableCoroutine { continuation ->
|
|
115
117
|
val resolveListener = object : NsdManager.ResolveListener {
|
|
116
118
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
|
117
|
-
|
|
119
|
+
loggy.e("Resolve failed: ${serviceInfo.serviceName}, error: $errorCode", id)
|
|
118
120
|
|
|
119
121
|
notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
|
|
120
122
|
continuation.resume(null) {}
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
|
124
|
-
|
|
126
|
+
loggy.d("Service resolved: ${serviceInfo.serviceName}", id)
|
|
125
127
|
|
|
126
128
|
if (!_isScanning) {
|
|
127
129
|
continuation.resume(null) {}
|
|
@@ -142,7 +144,7 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
|
|
|
142
144
|
manager.resolveService(service, resolveListener)
|
|
143
145
|
} catch (e: Exception) {
|
|
144
146
|
notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
|
|
145
|
-
|
|
147
|
+
loggy.e("Exception resolving service", id, e)
|
|
146
148
|
continuation.resume(null) {}
|
|
147
149
|
}
|
|
148
150
|
}
|
|
@@ -154,10 +156,10 @@ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, servic
|
|
|
154
156
|
serviceCache[serviceKey] = scanResult
|
|
155
157
|
notifyScanResultsListeners()
|
|
156
158
|
}
|
|
157
|
-
} ?:
|
|
159
|
+
} ?: loggy.w("Failed to resolve service: $serviceKey", id)
|
|
158
160
|
|
|
159
161
|
} catch (e: Exception) {
|
|
160
|
-
|
|
162
|
+
loggy.e("Error during service resolution", id, e)
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -168,7 +170,7 @@ private fun BonjourZeroconf.extractScanResult(serviceInfo: NsdServiceInfo): Scan
|
|
|
168
170
|
|
|
169
171
|
val (ipv4, ipv6) = when {
|
|
170
172
|
host.address.size == 4 -> host.hostAddress to null
|
|
171
|
-
host.address.size == 16 -> null to formatIPv6Address(host.address)
|
|
173
|
+
host.address.size == 16 -> null to formatIPv6Address(host.address, host as? Inet6Address)
|
|
172
174
|
else -> null to null
|
|
173
175
|
}
|
|
174
176
|
|
|
@@ -181,16 +183,26 @@ private fun BonjourZeroconf.extractScanResult(serviceInfo: NsdServiceInfo): Scan
|
|
|
181
183
|
)
|
|
182
184
|
} catch (e: Exception) {
|
|
183
185
|
notifyScanFailListeners(BonjourFail.EXTRACTION_FAILED)
|
|
184
|
-
|
|
186
|
+
loggy.e("Failed to extract scan result", id, e)
|
|
185
187
|
null
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
private fun formatIPv6Address(bytes: ByteArray): String {
|
|
191
|
+
private fun formatIPv6Address(bytes: ByteArray, inet6Address: Inet6Address? = null): String {
|
|
190
192
|
require(bytes.size == 16) { "IPv6 address must be 16 bytes" }
|
|
191
193
|
|
|
192
|
-
|
|
194
|
+
val formatted = (0 until 16 step 2).joinToString(":") { i ->
|
|
193
195
|
val segment = ((bytes[i].toInt() and 0xFF) shl 8) or (bytes[i + 1].toInt() and 0xFF)
|
|
194
196
|
segment.toString(16)
|
|
195
197
|
}
|
|
198
|
+
|
|
199
|
+
if (formatted.startsWith("fe80:") && inet6Address != null) {
|
|
200
|
+
val interfaceName = inet6Address.scopedInterface?.name
|
|
201
|
+
?: NetworkInterface.getByIndex(inet6Address.scopeId)?.name
|
|
202
|
+
if (interfaceName != null) {
|
|
203
|
+
return "$formatted%$interfaceName"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return formatted
|
|
196
208
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.dawidzawada.bonjourzeroconf
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.TAG
|
|
3
|
+
import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.loggy
|
|
5
4
|
import kotlinx.coroutines.Dispatchers
|
|
6
5
|
import kotlinx.coroutines.launch
|
|
7
6
|
|
|
@@ -12,7 +11,7 @@ fun BonjourZeroconf.notifyScanResultsListeners() {
|
|
|
12
11
|
try {
|
|
13
12
|
listener(results)
|
|
14
13
|
} catch (e: Exception) {
|
|
15
|
-
|
|
14
|
+
loggy.e("Error notifying scan results listener", id, e)
|
|
16
15
|
}
|
|
17
16
|
}
|
|
18
17
|
}
|
|
@@ -25,7 +24,7 @@ fun BonjourZeroconf.updateScanningState(newState: Boolean) {
|
|
|
25
24
|
try {
|
|
26
25
|
listener(newState)
|
|
27
26
|
} catch (e: Exception) {
|
|
28
|
-
|
|
27
|
+
loggy.e("Error notifying scan state listener", id, e)
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
}
|
|
@@ -38,7 +37,7 @@ fun BonjourZeroconf.notifyScanFailListeners(fail: BonjourFail) {
|
|
|
38
37
|
try {
|
|
39
38
|
listener(fail)
|
|
40
39
|
} catch (e: Exception) {
|
|
41
|
-
|
|
40
|
+
loggy.e("Error notifying scan fail listener", id, e)
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
}
|
package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt
CHANGED
|
@@ -3,7 +3,6 @@ package com.margelo.nitro.dawidzawada.bonjourzeroconf
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.net.nsd.NsdManager
|
|
5
5
|
import android.net.nsd.NsdServiceInfo
|
|
6
|
-
import android.util.Log
|
|
7
6
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
8
7
|
import java.util.UUID
|
|
9
8
|
import java.util.concurrent.ConcurrentHashMap
|
|
@@ -20,10 +19,13 @@ import kotlinx.coroutines.sync.Mutex
|
|
|
20
19
|
class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
21
20
|
|
|
22
21
|
companion object {
|
|
23
|
-
internal const val TAG = "BonjourZeroconf"
|
|
24
22
|
internal const val DEFAULT_RESOLVE_TIMEOUT_MS = 10_000L
|
|
23
|
+
internal val legacyResolveMutex = Mutex()
|
|
24
|
+
internal val loggy = Loggy(BonjourZeroconf::class)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
override var id: String? = null
|
|
28
|
+
|
|
27
29
|
@Volatile
|
|
28
30
|
internal var _isScanning = false
|
|
29
31
|
|
|
@@ -36,7 +38,6 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
36
38
|
internal val serviceCache = ConcurrentHashMap<String, ScanResult>()
|
|
37
39
|
internal val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
38
40
|
internal val resolveScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
39
|
-
internal val legacyResolveMutex = Mutex()
|
|
40
41
|
|
|
41
42
|
override val isScanning: Boolean
|
|
42
43
|
get() = _isScanning
|
|
@@ -59,12 +60,12 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
59
60
|
|
|
60
61
|
currentDiscoveryListener = createDiscoveryListener(resolveTimeout).also { listener ->
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
+
loggy.i("Starting scan for type: $type", id)
|
|
63
64
|
updateScanningState(true)
|
|
64
65
|
nsdManager?.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
|
|
65
66
|
?: throw IllegalStateException("NsdManager is not initialized")
|
|
66
67
|
} catch (e: Exception) {
|
|
67
|
-
|
|
68
|
+
loggy.e("Failed to start discovery", id, e)
|
|
68
69
|
updateScanningState(false)
|
|
69
70
|
throw RuntimeException("Failed to start service discovery: ${e.message}", e)
|
|
70
71
|
}
|
|
@@ -96,9 +97,9 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
96
97
|
currentDiscoveryListener?.let { listener ->
|
|
97
98
|
try {
|
|
98
99
|
nsdManager?.stopServiceDiscovery(listener)
|
|
99
|
-
|
|
100
|
+
loggy.i("Stopped service discovery", id)
|
|
100
101
|
} catch (e: Exception) {
|
|
101
|
-
|
|
102
|
+
loggy.e("Error stopping discovery", id, e)
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -116,7 +117,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
116
117
|
try {
|
|
117
118
|
onResult(currentResults)
|
|
118
119
|
} catch (e: Exception) {
|
|
119
|
-
|
|
120
|
+
loggy.e("Scan results listener error", throwable = e)
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
}
|
|
@@ -134,7 +135,7 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
134
135
|
try {
|
|
135
136
|
onChange(_isScanning)
|
|
136
137
|
} catch (e: Exception) {
|
|
137
|
-
|
|
138
|
+
loggy.e("Scan state listener error", throwable = e)
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -155,12 +156,12 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
155
156
|
private fun createDiscoveryListener(resolveTimeout: Long) = object : NsdManager.DiscoveryListener {
|
|
156
157
|
|
|
157
158
|
override fun onDiscoveryStarted(serviceType: String) {
|
|
158
|
-
|
|
159
|
+
loggy.d("Discovery started: $serviceType", id)
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
override fun onServiceFound(service: NsdServiceInfo) {
|
|
162
163
|
val serviceKey = createServiceKey(service)
|
|
163
|
-
|
|
164
|
+
loggy.d("Service found: ${service.serviceName} (key: $serviceKey)", id)
|
|
164
165
|
|
|
165
166
|
if (serviceCache.containsKey(serviceKey)) {
|
|
166
167
|
return
|
|
@@ -170,14 +171,14 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
170
171
|
try {
|
|
171
172
|
resolveService(service, serviceKey, resolveTimeout)
|
|
172
173
|
} catch (e: Exception) {
|
|
173
|
-
|
|
174
|
+
loggy.e("Error resolving service: $serviceKey", id, e)
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
override fun onServiceLost(service: NsdServiceInfo) {
|
|
179
180
|
val serviceKey = createServiceKey(service)
|
|
180
|
-
|
|
181
|
+
loggy.d("Service lost: ${service.serviceName} (key: $serviceKey)", id)
|
|
181
182
|
|
|
182
183
|
serviceCache.remove(serviceKey)?.let {
|
|
183
184
|
notifyScanResultsListeners()
|
|
@@ -185,19 +186,19 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
override fun onDiscoveryStopped(serviceType: String) {
|
|
188
|
-
|
|
189
|
+
loggy.d("Discovery stopped: $serviceType", id)
|
|
189
190
|
updateScanningState(false)
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
|
193
|
-
|
|
194
|
+
loggy.e("Start discovery failed: $serviceType, error: $errorCode", id)
|
|
194
195
|
notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
|
|
195
196
|
updateScanningState(false)
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
|
199
200
|
notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
|
|
200
|
-
|
|
201
|
+
loggy.e("Stop discovery failed: $serviceType, error: $errorCode", id)
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -205,3 +206,4 @@ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
|
|
|
205
206
|
return "${service.serviceName}.${service.serviceType}"
|
|
206
207
|
}
|
|
207
208
|
}
|
|
209
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.margelo.nitro.dawidzawada.bonjourzeroconf
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import kotlin.reflect.KClass
|
|
5
|
+
|
|
6
|
+
internal class Loggy(clazz: KClass<*>) {
|
|
7
|
+
private val tag = clazz.simpleName ?: "Unknown"
|
|
8
|
+
|
|
9
|
+
private fun format(message: String, id: String?): String =
|
|
10
|
+
if (id != null) "($id) $message" else message
|
|
11
|
+
|
|
12
|
+
fun d(message: String, id: String? = null) = Log.d(tag, format(message, id))
|
|
13
|
+
fun i(message: String, id: String? = null) = Log.i(tag, format(message, id))
|
|
14
|
+
fun w(message: String, id: String? = null) = Log.w(tag, format(message, id))
|
|
15
|
+
fun e(message: String, id: String? = null, throwable: Throwable? = null) = Log.e(tag, format(message, id), throwable)
|
|
16
|
+
}
|
|
@@ -10,11 +10,11 @@ extension BonjourZeroconf {
|
|
|
10
10
|
/// Resolve a service to get its IP address and port using async/await
|
|
11
11
|
internal func resolveService(result: NWBrowser.Result, name: String, timeout: TimeInterval) async -> ScanResult? {
|
|
12
12
|
let taskId = UUID()
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
do {
|
|
15
15
|
return try await withCheckedThrowingContinuation { continuation in
|
|
16
16
|
let connection = NWConnection(to: result.endpoint, using: .tcp)
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
resolveLock.lock()
|
|
19
19
|
guard _isScanning else {
|
|
20
20
|
resolveLock.unlock()
|
|
@@ -23,25 +23,25 @@ extension BonjourZeroconf {
|
|
|
23
23
|
}
|
|
24
24
|
activeConnections[taskId] = connection
|
|
25
25
|
resolveLock.unlock()
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
final class ResumeBox { var hasResumed = false }
|
|
28
28
|
let box = ResumeBox()
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
let timeoutTask = DispatchWorkItem { [weak self] in
|
|
31
31
|
guard let self = self else { return }
|
|
32
32
|
guard !box.hasResumed else { return }
|
|
33
33
|
box.hasResumed = true
|
|
34
|
-
Loggy.log(.debug, message: "Timeout resolving \(name)")
|
|
34
|
+
Loggy.log(.debug, id: self.id, message: "Timeout resolving \(name)")
|
|
35
35
|
continuation.resume(throwing: AddressResolverError.timeout)
|
|
36
36
|
self.cleanupResolve(connection: connection, taskId: taskId)
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
resolveLock.lock()
|
|
40
40
|
activeTimeouts[taskId] = timeoutTask
|
|
41
41
|
resolveLock.unlock()
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
networkQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTask)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
connection.stateUpdateHandler = { [weak self] state in
|
|
46
46
|
guard let self = self else { return }
|
|
47
47
|
switch state {
|
|
@@ -49,7 +49,7 @@ extension BonjourZeroconf {
|
|
|
49
49
|
timeoutTask.cancel()
|
|
50
50
|
guard !box.hasResumed else { return }
|
|
51
51
|
box.hasResumed = true
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
if let remoteEndpoint = connection.currentPath?.remoteEndpoint,
|
|
54
54
|
let scanResult = self.extractIPAndPort(from: remoteEndpoint, serviceName: name) {
|
|
55
55
|
continuation.resume(returning: scanResult)
|
|
@@ -57,26 +57,26 @@ extension BonjourZeroconf {
|
|
|
57
57
|
continuation.resume(throwing: AddressResolverError.extractionFailed)
|
|
58
58
|
}
|
|
59
59
|
self.cleanupResolve(connection: connection, taskId: taskId)
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
case .failed(let error):
|
|
62
62
|
timeoutTask.cancel()
|
|
63
63
|
guard !box.hasResumed else { return }
|
|
64
64
|
box.hasResumed = true
|
|
65
|
-
Loggy.log(.error, message: "Failed to resolve service \(name): \(error.localizedDescription)")
|
|
65
|
+
Loggy.log(.error, id: self.id, message: "Failed to resolve service \(name): \(error.localizedDescription)")
|
|
66
66
|
continuation.resume(throwing: error)
|
|
67
67
|
self.cleanupResolve(connection: connection, taskId: taskId)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
case .waiting(let error):
|
|
70
|
-
Loggy.log(.debug, message: "Connection waiting for \(name): \(error.localizedDescription)")
|
|
71
|
-
|
|
70
|
+
Loggy.log(.debug, id: self.id, message: "Connection waiting for \(name): \(error.localizedDescription)")
|
|
71
|
+
|
|
72
72
|
case .cancelled:
|
|
73
73
|
break
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
default:
|
|
76
76
|
break
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
connection.start(queue: networkQueue)
|
|
81
81
|
}
|
|
82
82
|
} catch let error as AddressResolverError {
|
|
@@ -86,7 +86,7 @@ extension BonjourZeroconf {
|
|
|
86
86
|
case .extractionFailed:
|
|
87
87
|
notifyScanFailListeners(with: BonjourFail.extractionFailed)
|
|
88
88
|
case .cancelled:
|
|
89
|
-
Loggy.log(.debug, message: "Scanning stopped, cancelling address resolution")
|
|
89
|
+
Loggy.log(.debug, id: self.id, message: "Scanning stopped, cancelling address resolution")
|
|
90
90
|
break
|
|
91
91
|
}
|
|
92
92
|
return nil
|
|
@@ -94,10 +94,10 @@ extension BonjourZeroconf {
|
|
|
94
94
|
return nil
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
/// Cancels ongoing address resolve process
|
|
99
99
|
internal func cancelAddressResolving() {
|
|
100
|
-
Loggy.log(.debug, message: "Cancelling Address Resolving")
|
|
100
|
+
Loggy.log(.debug, id: id, message: "Cancelling Address Resolving")
|
|
101
101
|
resolveLock.lock()
|
|
102
102
|
activeTimeouts.values.forEach { $0.cancel() }
|
|
103
103
|
activeTimeouts.removeAll()
|
|
@@ -114,7 +114,7 @@ extension BonjourZeroconf {
|
|
|
114
114
|
activeTimeouts.removeValue(forKey: taskId)
|
|
115
115
|
resolveLock.unlock()
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
/// Extract IP address and port from an endpoint
|
|
119
119
|
private func extractIPAndPort(from endpoint: NWEndpoint, serviceName: String) -> ScanResult? {
|
|
120
120
|
switch endpoint {
|
|
@@ -123,17 +123,17 @@ extension BonjourZeroconf {
|
|
|
123
123
|
var ipv6: String?
|
|
124
124
|
var hostname: String?
|
|
125
125
|
let portNumber = Int(port.rawValue)
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
switch host {
|
|
128
128
|
case .ipv4(let address):
|
|
129
129
|
ipv4 = address.rawValue.map(String.init).joined(separator: ".")
|
|
130
|
-
Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv4: \(ipv4!), Port: \(portNumber)")
|
|
131
|
-
|
|
130
|
+
Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> IPv4: \(ipv4!), Port: \(portNumber)")
|
|
131
|
+
|
|
132
132
|
case .ipv6(let address):
|
|
133
133
|
let formatted = stride(from: 0, to: address.rawValue.count, by: 2).map { i in
|
|
134
134
|
String(format: "%02x%02x", address.rawValue[i], address.rawValue[i + 1])
|
|
135
135
|
}.joined(separator: ":")
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
if formatted.hasPrefix("fe80:") {
|
|
138
138
|
if let interface = endpoint.interface?.name {
|
|
139
139
|
ipv6 = "\(formatted)%\(interface)"
|
|
@@ -143,17 +143,17 @@ extension BonjourZeroconf {
|
|
|
143
143
|
} else {
|
|
144
144
|
ipv6 = formatted
|
|
145
145
|
}
|
|
146
|
-
Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv6: \(ipv6!), Port: \(portNumber)")
|
|
147
|
-
|
|
146
|
+
Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> IPv6: \(ipv6!), Port: \(portNumber)")
|
|
147
|
+
|
|
148
148
|
case .name(let name, _):
|
|
149
149
|
hostname = name
|
|
150
|
-
Loggy.log(.debug, message: "Resolved \(serviceName) -> Hostname: \(hostname ?? "nil"), Port: \(portNumber)")
|
|
151
|
-
|
|
150
|
+
Loggy.log(.debug, id: id, message: "Resolved \(serviceName) -> Hostname: \(hostname ?? "nil"), Port: \(portNumber)")
|
|
151
|
+
|
|
152
152
|
@unknown default:
|
|
153
|
-
Loggy.log(.debug, message: "Unknown host type for \(serviceName)")
|
|
153
|
+
Loggy.log(.debug, id: id, message: "Unknown host type for \(serviceName)")
|
|
154
154
|
return nil
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
return ScanResult(
|
|
158
158
|
name: serviceName,
|
|
159
159
|
ipv4: ipv4,
|
|
@@ -161,9 +161,9 @@ extension BonjourZeroconf {
|
|
|
161
161
|
hostname: hostname,
|
|
162
162
|
port: Double(portNumber)
|
|
163
163
|
)
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
default:
|
|
166
|
-
Loggy.log(.warning, message: "Unexpected endpoint format for \(serviceName)")
|
|
166
|
+
Loggy.log(.warning, id: id, message: "Unexpected endpoint format for \(serviceName)")
|
|
167
167
|
return nil
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -7,20 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
extension BonjourZeroconf {
|
|
9
9
|
internal func notifyScanResultsListeners(with results: [ScanResult]) {
|
|
10
|
-
|
|
11
|
-
listener(results)
|
|
12
|
-
}
|
|
10
|
+
Task { await listenerStore.notifyScanResults(with: results) }
|
|
13
11
|
}
|
|
14
|
-
|
|
12
|
+
|
|
15
13
|
internal func notifyScanStateListeners(with isScanningState: Bool) {
|
|
16
|
-
|
|
17
|
-
listener(isScanningState)
|
|
18
|
-
}
|
|
14
|
+
Task { await listenerStore.notifyScanState(with: isScanningState) }
|
|
19
15
|
}
|
|
20
|
-
|
|
16
|
+
|
|
21
17
|
internal func notifyScanFailListeners(with fail: BonjourFail) {
|
|
22
|
-
|
|
23
|
-
listener(fail)
|
|
24
|
-
}
|
|
18
|
+
Task { await listenerStore.notifyScanFail(with: fail) }
|
|
25
19
|
}
|
|
26
20
|
}
|