react-native-zeroconf 0.17.0 → 0.17.1
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 +2 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +5 -0
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +22 -1
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +69 -12
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceInfoCallbacks.java +50 -13
- package/dist/index.js +1 -0
- package/index.d.ts +4 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/windows/RNZeroconf/RNZeroconf.cpp +2 -1
- package/windows/RNZeroconf/ZeroconfCore.cpp +56 -3
- package/windows/RNZeroconf/ZeroconfCore.h +3 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src=".github/banner.png" alt="react-native-zeroconf: Bonjour / mDNS discovery and publishing for React Native, on iOS, Android, macOS, tvOS and Expo" width="100%">
|
|
2
|
+
<a href="https://zeroconf.balthazar.dev"><img src=".github/banner.png" alt="react-native-zeroconf: Bonjour / mDNS discovery and publishing for React Native, on iOS, Android, Windows, macOS, tvOS and Expo" width="100%"></a>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
Find the services other devices advertise on the local network (printers, speakers, cameras, your own servers) and advertise your own, on iOS, Android, macOS, tvOS and Windows.
|
|
@@ -13,7 +13,7 @@ yarn add react-native-zeroconf
|
|
|
13
13
|
cd ios && pod install
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
On iOS, declare the service types you use in `NSBonjourServices`, see [Permissions and Setup](https://zeroconf.balthazar.dev/Permissions-and-Setup). Expo works with development builds, see [Installation](https://zeroconf.balthazar.dev/Installation).
|
|
16
|
+
On iOS, declare the service types you use in `NSBonjourServices`, see [Permissions and Setup](https://zeroconf.balthazar.dev/Permissions-and-Setup). Expo works with development builds, see [Installation with Expo](https://zeroconf.balthazar.dev/Installation-Expo).
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
@@ -21,6 +21,8 @@ public class ZeroconfOptions {
|
|
|
21
21
|
@Nullable public String networkInterface;
|
|
22
22
|
/** resolveService: seconds before giving up */
|
|
23
23
|
public double timeoutSeconds = 5;
|
|
24
|
+
/** Scan: seconds to resolve each service, retried once before a TIMEOUT error */
|
|
25
|
+
public double resolveTimeoutSeconds = 5;
|
|
24
26
|
|
|
25
27
|
public static ZeroconfOptions from(@Nullable ReadableMap map) {
|
|
26
28
|
ZeroconfOptions options = new ZeroconfOptions();
|
|
@@ -42,6 +44,9 @@ public class ZeroconfOptions {
|
|
|
42
44
|
if (map.hasKey("timeout") && map.getType("timeout") == ReadableType.Number && map.getDouble("timeout") > 0) {
|
|
43
45
|
options.timeoutSeconds = map.getDouble("timeout");
|
|
44
46
|
}
|
|
47
|
+
if (map.hasKey("resolveTimeout") && map.getType("resolveTimeout") == ReadableType.Number && map.getDouble("resolveTimeout") > 0) {
|
|
48
|
+
options.resolveTimeoutSeconds = map.getDouble("resolveTimeout");
|
|
49
|
+
}
|
|
45
50
|
return options;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -66,6 +66,7 @@ public class DnssdImpl implements Zeroconf {
|
|
|
66
66
|
final String scanId;
|
|
67
67
|
final boolean typesOnly;
|
|
68
68
|
int ifIndex = DNSSD.ALL_INTERFACES;
|
|
69
|
+
long resolveTimeoutMs = 5000;
|
|
69
70
|
DNSSDService browse;
|
|
70
71
|
// A service is reported once per network interface, count them so found/remove are emitted once
|
|
71
72
|
final Map<String, Integer> foundInterfaces = new HashMap<>();
|
|
@@ -87,6 +88,9 @@ public class DnssdImpl implements Zeroconf {
|
|
|
87
88
|
int port;
|
|
88
89
|
Map<String, String> txt = new LinkedHashMap<>();
|
|
89
90
|
final Set<String> addresses = new LinkedHashSet<>();
|
|
91
|
+
// Scans: emitted once an address is known, retried once after a timeout
|
|
92
|
+
boolean emitted;
|
|
93
|
+
boolean retried;
|
|
90
94
|
|
|
91
95
|
Resolve(String name) {
|
|
92
96
|
this.name = name;
|
|
@@ -144,6 +148,7 @@ public class DnssdImpl implements Zeroconf {
|
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
scan.ifIndex = ifIndex;
|
|
151
|
+
scan.resolveTimeoutMs = (long) (options.resolveTimeoutSeconds * 1000);
|
|
147
152
|
// "_ipp._tcp,_printer" browses the _printer subtype
|
|
148
153
|
String regType = String.format("_%s._%s", type, protocol);
|
|
149
154
|
if (options.subtype != null) {
|
|
@@ -254,10 +259,25 @@ public class DnssdImpl implements Zeroconf {
|
|
|
254
259
|
|
|
255
260
|
// Resolve: host, port and TXT record, then the host's IPv4 and IPv6 addresses
|
|
256
261
|
|
|
257
|
-
private void startResolve(@Nullable final Scan scan, int ifIndex, final String serviceName, String regType, String domain, @Nullable Resolve existing) {
|
|
262
|
+
private void startResolve(@Nullable final Scan scan, final int ifIndex, final String serviceName, final String regType, final String domain, @Nullable Resolve existing) {
|
|
258
263
|
final Resolve resolve = existing != null ? existing : new Resolve(serviceName);
|
|
259
264
|
if (scan != null) {
|
|
260
265
|
scan.resolves.put(serviceName, resolve);
|
|
266
|
+
// DNSSD gives up silently after its own timeout, report it before
|
|
267
|
+
mainHandler.postDelayed(() -> executor.execute(() -> {
|
|
268
|
+
if (!isCurrent(scan, resolve) || resolve.emitted) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
resolve.stop();
|
|
272
|
+
// Slow devices can time out, retry once before reporting the error
|
|
273
|
+
if (!resolve.retried) {
|
|
274
|
+
resolve.retried = true;
|
|
275
|
+
startResolve(scan, ifIndex, serviceName, regType, domain, resolve);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
scan.resolves.remove(serviceName);
|
|
279
|
+
zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scan.scanId);
|
|
280
|
+
}), scan.resolveTimeoutMs);
|
|
261
281
|
}
|
|
262
282
|
try {
|
|
263
283
|
resolve.operations.add(dnssd.resolve(0, ifIndex, serviceName, regType, domain, new ResolveListener() {
|
|
@@ -306,6 +326,7 @@ public class DnssdImpl implements Zeroconf {
|
|
|
306
326
|
return;
|
|
307
327
|
}
|
|
308
328
|
// Emitted for each address, the first one is usable right away
|
|
329
|
+
resolve.emitted = true;
|
|
309
330
|
sendScanEvent(ZeroconfModule.EVENT_RESOLVE, resolveToMap(resolve), scan.scanId);
|
|
310
331
|
});
|
|
311
332
|
}
|
|
@@ -63,6 +63,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
63
63
|
|
|
64
64
|
private static class NsdScan {
|
|
65
65
|
final String scanId;
|
|
66
|
+
double resolveTimeoutSeconds = 5;
|
|
66
67
|
NsdManager.DiscoveryListener discoveryListener;
|
|
67
68
|
// Android 14+: services are followed with registerServiceInfoCallback, null before
|
|
68
69
|
@Nullable ServiceInfoCallbacks infoCallbacks;
|
|
@@ -83,11 +84,14 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
83
84
|
final NsdServiceInfo serviceInfo;
|
|
84
85
|
// resolveService(): settled instead of emitting events
|
|
85
86
|
@Nullable final Promise promise;
|
|
87
|
+
// Seconds before the TIMEOUT error, counted from the start of the resolve
|
|
88
|
+
final double timeoutSeconds;
|
|
86
89
|
|
|
87
|
-
PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise) {
|
|
90
|
+
PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise, double timeoutSeconds) {
|
|
88
91
|
this.scanId = scanId;
|
|
89
92
|
this.serviceInfo = serviceInfo;
|
|
90
93
|
this.promise = promise;
|
|
94
|
+
this.timeoutSeconds = timeoutSeconds;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -104,6 +108,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
104
108
|
this.stop(scanId);
|
|
105
109
|
|
|
106
110
|
final NsdScan scan = new NsdScan(scanId);
|
|
111
|
+
scan.resolveTimeoutSeconds = options.resolveTimeoutSeconds;
|
|
107
112
|
final boolean typesOnly = ZeroconfModule.isServiceTypesScan(type, protocol);
|
|
108
113
|
|
|
109
114
|
Network network = null;
|
|
@@ -154,7 +159,8 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
if (Build.VERSION.SDK_INT >= 34) {
|
|
157
|
-
|
|
162
|
+
long timeoutMs = (long) (options.resolveTimeoutSeconds * 1000);
|
|
163
|
+
scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, mHandler, timeoutMs, new ServiceInfoCallbacks.Listener() {
|
|
158
164
|
@Override
|
|
159
165
|
public void onServiceUpdated(NsdServiceInfo serviceInfo) {
|
|
160
166
|
if (getHostAddresses(serviceInfo).isEmpty()) {
|
|
@@ -167,6 +173,11 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
167
173
|
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
168
174
|
zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode), serviceInfo.getServiceName(), scanId);
|
|
169
175
|
}
|
|
176
|
+
|
|
177
|
+
@Override
|
|
178
|
+
public void onTimeout(NsdServiceInfo serviceInfo) {
|
|
179
|
+
sendResolveTimeout(serviceInfo.getServiceName(), scanId);
|
|
180
|
+
}
|
|
170
181
|
});
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -204,7 +215,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
204
215
|
if (scan.infoCallbacks != null) {
|
|
205
216
|
scan.infoCallbacks.register(serviceInfo);
|
|
206
217
|
} else {
|
|
207
|
-
enqueueResolve(
|
|
218
|
+
enqueueResolve(scan, serviceInfo);
|
|
208
219
|
}
|
|
209
220
|
}
|
|
210
221
|
|
|
@@ -278,7 +289,8 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
278
289
|
synchronized (mResolveQueue) {
|
|
279
290
|
Iterator<PendingResolve> iterator = mResolveQueue.iterator();
|
|
280
291
|
while (iterator.hasNext()) {
|
|
281
|
-
|
|
292
|
+
// resolveService() entries have no scan id
|
|
293
|
+
if (scanId.equals(iterator.next().scanId)) {
|
|
282
294
|
iterator.remove();
|
|
283
295
|
}
|
|
284
296
|
}
|
|
@@ -384,7 +396,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
384
396
|
if (Build.VERSION.SDK_INT < 34) {
|
|
385
397
|
// One resolve at a time before Android 14, shares the queue of the scans
|
|
386
398
|
synchronized (mResolveQueue) {
|
|
387
|
-
mResolveQueue.add(new PendingResolve(null, serviceInfo, promise));
|
|
399
|
+
mResolveQueue.add(new PendingResolve(null, serviceInfo, promise, options.timeoutSeconds));
|
|
388
400
|
}
|
|
389
401
|
resolveNext();
|
|
390
402
|
return;
|
|
@@ -524,13 +536,19 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
524
536
|
return reactApplicationContext;
|
|
525
537
|
}
|
|
526
538
|
|
|
527
|
-
private void enqueueResolve(
|
|
539
|
+
private void enqueueResolve(NsdScan scan, NsdServiceInfo serviceInfo) {
|
|
540
|
+
// A resolve can't be cancelled before Android 14, so there is no retry: waiting twice as long
|
|
541
|
+
// matches the total time of the other implementations, which retry once
|
|
528
542
|
synchronized (mResolveQueue) {
|
|
529
|
-
mResolveQueue.add(new PendingResolve(scanId, serviceInfo, null));
|
|
543
|
+
mResolveQueue.add(new PendingResolve(scan.scanId, serviceInfo, null, scan.resolveTimeoutSeconds * 2));
|
|
530
544
|
}
|
|
531
545
|
resolveNext();
|
|
532
546
|
}
|
|
533
547
|
|
|
548
|
+
private void sendResolveTimeout(String serviceName, String scanId) {
|
|
549
|
+
zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scanId);
|
|
550
|
+
}
|
|
551
|
+
|
|
534
552
|
private void resolveNext() {
|
|
535
553
|
PendingResolve next;
|
|
536
554
|
synchronized (mResolveQueue) {
|
|
@@ -540,8 +558,11 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
540
558
|
next = mResolveQueue.poll();
|
|
541
559
|
mIsResolving = true;
|
|
542
560
|
}
|
|
561
|
+
final ZeroResolveListener listener = new ZeroResolveListener(next);
|
|
543
562
|
try {
|
|
544
|
-
getNsdManager().resolveService(next.serviceInfo,
|
|
563
|
+
getNsdManager().resolveService(next.serviceInfo, listener);
|
|
564
|
+
// The system keeps the resolve until it answers or fails, the queue waits for it
|
|
565
|
+
mHandler.postDelayed(listener::timedOut, (long) (next.timeoutSeconds * 1000));
|
|
545
566
|
} catch (Throwable e) {
|
|
546
567
|
Log.e(TAG, "resolveService failed", e);
|
|
547
568
|
String serviceName = next.serviceInfo.getServiceName();
|
|
@@ -558,25 +579,57 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
558
579
|
}
|
|
559
580
|
|
|
560
581
|
private class ZeroResolveListener implements NsdManager.ResolveListener {
|
|
582
|
+
private final PendingResolve pending;
|
|
561
583
|
@Nullable private final String scanId;
|
|
562
584
|
@Nullable private final Promise promise;
|
|
585
|
+
// Settled: answered, failed or timed out. A late answer after the timeout only frees the queue
|
|
586
|
+
private boolean settled = false;
|
|
563
587
|
|
|
564
|
-
ZeroResolveListener(
|
|
565
|
-
this.
|
|
566
|
-
this.
|
|
588
|
+
ZeroResolveListener(PendingResolve pending) {
|
|
589
|
+
this.pending = pending;
|
|
590
|
+
this.scanId = pending.scanId;
|
|
591
|
+
this.promise = pending.promise;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private synchronized boolean settle() {
|
|
595
|
+
if (settled) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
settled = true;
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
void timedOut() {
|
|
603
|
+
if (!settle()) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
String serviceName = pending.serviceInfo.getServiceName();
|
|
607
|
+
if (promise != null) {
|
|
608
|
+
ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName);
|
|
609
|
+
} else if (mScans.containsKey(scanId)) {
|
|
610
|
+
sendResolveTimeout(serviceName, scanId);
|
|
611
|
+
}
|
|
567
612
|
}
|
|
568
613
|
|
|
569
614
|
@Override
|
|
570
615
|
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
571
616
|
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
|
|
572
617
|
// Another resolve (possibly from another app or listener) is in flight: retry shortly
|
|
618
|
+
if (!settle()) {
|
|
619
|
+
onResolveDone();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
573
622
|
synchronized (mResolveQueue) {
|
|
574
|
-
mResolveQueue.addFirst(
|
|
623
|
+
mResolveQueue.addFirst(pending);
|
|
575
624
|
mIsResolving = false;
|
|
576
625
|
}
|
|
577
626
|
mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
|
|
578
627
|
return;
|
|
579
628
|
}
|
|
629
|
+
if (!settle()) {
|
|
630
|
+
onResolveDone();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
580
633
|
String message = "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode);
|
|
581
634
|
if (promise != null) {
|
|
582
635
|
ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName());
|
|
@@ -588,6 +641,10 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
588
641
|
|
|
589
642
|
@Override
|
|
590
643
|
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
|
644
|
+
if (!settle()) {
|
|
645
|
+
onResolveDone();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
591
648
|
if (promise != null) {
|
|
592
649
|
promise.resolve(serviceInfoToMap(serviceInfo));
|
|
593
650
|
} else if (mScans.containsKey(scanId)) {
|
|
@@ -3,6 +3,7 @@ package com.balthazargronon.RCTZeroconf.nsd;
|
|
|
3
3
|
import android.annotation.TargetApi;
|
|
4
4
|
import android.net.nsd.NsdManager;
|
|
5
5
|
import android.net.nsd.NsdServiceInfo;
|
|
6
|
+
import android.os.Handler;
|
|
6
7
|
import android.util.Log;
|
|
7
8
|
|
|
8
9
|
import java.util.ArrayList;
|
|
@@ -13,6 +14,7 @@ import java.util.concurrent.Executor;
|
|
|
13
14
|
/**
|
|
14
15
|
* Follows found services with NsdManager.registerServiceInfoCallback (Android 14+) instead of one-shot resolves:
|
|
15
16
|
* services can be followed concurrently, updates (addresses, TXT records) are delivered as they change.
|
|
17
|
+
* A service without addresses after the resolve timeout is followed again once, then reported with onTimeout.
|
|
16
18
|
* Only loaded on API 34+, so older devices never touch these APIs.
|
|
17
19
|
*/
|
|
18
20
|
@TargetApi(34)
|
|
@@ -23,34 +25,55 @@ class ServiceInfoCallbacks {
|
|
|
23
25
|
void onServiceUpdated(NsdServiceInfo serviceInfo);
|
|
24
26
|
|
|
25
27
|
void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
|
|
28
|
+
|
|
29
|
+
void onTimeout(NsdServiceInfo serviceInfo);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static class Followed {
|
|
33
|
+
NsdManager.ServiceInfoCallback callback;
|
|
34
|
+
Runnable timeout;
|
|
35
|
+
volatile boolean resolved;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
private final NsdManager nsdManager;
|
|
29
39
|
private final Executor executor;
|
|
40
|
+
private final Handler handler;
|
|
41
|
+
private final long timeoutMs;
|
|
30
42
|
private final Listener listener;
|
|
31
|
-
private final Map<String,
|
|
43
|
+
private final Map<String, Followed> followed = new ConcurrentHashMap<>();
|
|
32
44
|
|
|
33
|
-
ServiceInfoCallbacks(NsdManager nsdManager, Executor executor, Listener listener) {
|
|
45
|
+
ServiceInfoCallbacks(NsdManager nsdManager, Executor executor, Handler handler, long timeoutMs, Listener listener) {
|
|
34
46
|
this.nsdManager = nsdManager;
|
|
35
47
|
this.executor = executor;
|
|
48
|
+
this.handler = handler;
|
|
49
|
+
this.timeoutMs = timeoutMs;
|
|
36
50
|
this.listener = listener;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
void register(final NsdServiceInfo serviceInfo) {
|
|
40
|
-
|
|
41
|
-
if (callbacks.containsKey(name)) {
|
|
54
|
+
if (followed.containsKey(serviceInfo.getServiceName())) {
|
|
42
55
|
return;
|
|
43
56
|
}
|
|
57
|
+
follow(serviceInfo, false);
|
|
58
|
+
}
|
|
44
59
|
|
|
45
|
-
|
|
60
|
+
private void follow(final NsdServiceInfo serviceInfo, final boolean retried) {
|
|
61
|
+
final String name = serviceInfo.getServiceName();
|
|
62
|
+
final Followed entry = new Followed();
|
|
63
|
+
entry.callback = new NsdManager.ServiceInfoCallback() {
|
|
46
64
|
@Override
|
|
47
65
|
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
if (followed.remove(name, entry)) {
|
|
67
|
+
handler.removeCallbacks(entry.timeout);
|
|
68
|
+
listener.onRegistrationFailed(serviceInfo, errorCode);
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
@Override
|
|
53
73
|
public void onServiceUpdated(NsdServiceInfo updated) {
|
|
74
|
+
if (!updated.getHostAddresses().isEmpty()) {
|
|
75
|
+
entry.resolved = true;
|
|
76
|
+
}
|
|
54
77
|
listener.onServiceUpdated(updated);
|
|
55
78
|
}
|
|
56
79
|
|
|
@@ -63,18 +86,32 @@ class ServiceInfoCallbacks {
|
|
|
63
86
|
public void onServiceInfoCallbackUnregistered() {
|
|
64
87
|
}
|
|
65
88
|
};
|
|
66
|
-
|
|
89
|
+
entry.timeout = () -> {
|
|
90
|
+
if (followed.get(name) != entry || entry.resolved) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
unregister(name);
|
|
94
|
+
// Slow devices can time out, follow again once before reporting the error
|
|
95
|
+
if (!retried) {
|
|
96
|
+
follow(serviceInfo, true);
|
|
97
|
+
} else {
|
|
98
|
+
listener.onTimeout(serviceInfo);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
followed.put(name, entry);
|
|
67
102
|
Log.d(TAG, "Following " + name + " with registerServiceInfoCallback");
|
|
68
|
-
nsdManager.registerServiceInfoCallback(serviceInfo, executor, callback);
|
|
103
|
+
nsdManager.registerServiceInfoCallback(serviceInfo, executor, entry.callback);
|
|
104
|
+
handler.postDelayed(entry.timeout, timeoutMs);
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
void unregister(String name) {
|
|
72
|
-
|
|
73
|
-
if (
|
|
108
|
+
Followed entry = followed.remove(name);
|
|
109
|
+
if (entry == null) {
|
|
74
110
|
return;
|
|
75
111
|
}
|
|
112
|
+
handler.removeCallbacks(entry.timeout);
|
|
76
113
|
try {
|
|
77
|
-
nsdManager.unregisterServiceInfoCallback(callback);
|
|
114
|
+
nsdManager.unregisterServiceInfoCallback(entry.callback);
|
|
78
115
|
} catch (IllegalArgumentException e) {
|
|
79
116
|
// Registration failed or was already unregistered
|
|
80
117
|
Log.w(TAG, "unregisterServiceInfoCallback failed", e);
|
|
@@ -82,7 +119,7 @@ class ServiceInfoCallbacks {
|
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
void unregisterAll() {
|
|
85
|
-
for (String name : new ArrayList<>(
|
|
122
|
+
for (String name : new ArrayList<>(followed.keySet())) {
|
|
86
123
|
unregister(name);
|
|
87
124
|
}
|
|
88
125
|
}
|
package/dist/index.js
CHANGED
package/index.d.ts
CHANGED
|
@@ -36,7 +36,10 @@ export interface ScanOptions {
|
|
|
36
36
|
domain?: string
|
|
37
37
|
/** Android only, defaults to `NSD` */
|
|
38
38
|
implType?: ImplType
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Seconds to try resolving each service before retrying once, then emitting an `error` with code `'TIMEOUT'`.
|
|
41
|
+
* Defaults to `5`. Android `NSD` before Android 14 can't retry a resolve, the error comes after twice this time
|
|
42
|
+
*/
|
|
40
43
|
resolveTimeout?: number
|
|
41
44
|
/** Only find services registered with this subtype, e.g. `'printer'` */
|
|
42
45
|
subtype?: string
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -140,7 +140,8 @@ void RNZeroconf::Emit(std::wstring_view eventName, React::JSValueObject body) no
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
void RNZeroconf::scan(std::string scanId, std::string type, std::string protocol, std::string domain, React::JSValue options) noexcept {
|
|
143
|
-
m_zeroconf->Scan(scanId, Wide(type), Wide(protocol), Wide(domain), StringOption(options, "subtype"), StringOption(options, "networkInterface")
|
|
143
|
+
m_zeroconf->Scan(scanId, Wide(type), Wide(protocol), Wide(domain), StringOption(options, "subtype"), StringOption(options, "networkInterface"),
|
|
144
|
+
NumberOption(options, "resolveTimeout"));
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
void RNZeroconf::stop(React::JSValue scanId) noexcept {
|
|
@@ -38,6 +38,8 @@ struct Resolve : Operation {
|
|
|
38
38
|
DNS_SERVICE_RESOLVE_REQUEST request{};
|
|
39
39
|
DNS_SERVICE_CANCEL cancel{};
|
|
40
40
|
bool done = false;
|
|
41
|
+
// Scans: retried once after a timeout
|
|
42
|
+
bool retried = false;
|
|
41
43
|
ServiceCallback promiseResolve;
|
|
42
44
|
ErrorCallback promiseReject;
|
|
43
45
|
};
|
|
@@ -50,6 +52,7 @@ struct Browse : Operation {
|
|
|
50
52
|
std::wstring queryName;
|
|
51
53
|
bool typesOnly = false;
|
|
52
54
|
ULONG interfaceIndex = 0;
|
|
55
|
+
DWORD resolveTimeoutMs = 5000;
|
|
53
56
|
DNS_SERVICE_BROWSE_REQUEST request{};
|
|
54
57
|
DNS_SERVICE_CANCEL cancel{};
|
|
55
58
|
// Lower-case PTR target -> name reported to JavaScript
|
|
@@ -362,7 +365,8 @@ void Zeroconf::Scan(
|
|
|
362
365
|
const std::wstring &protocol,
|
|
363
366
|
const std::wstring &domain,
|
|
364
367
|
const std::wstring &subtype,
|
|
365
|
-
const std::wstring &networkInterface
|
|
368
|
+
const std::wstring &networkInterface,
|
|
369
|
+
double resolveTimeoutSeconds) {
|
|
366
370
|
After after;
|
|
367
371
|
{
|
|
368
372
|
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
@@ -370,6 +374,7 @@ void Zeroconf::Scan(
|
|
|
370
374
|
|
|
371
375
|
auto browse = std::make_shared<Browse>(this);
|
|
372
376
|
browse->scanId = scanId;
|
|
377
|
+
browse->resolveTimeoutMs = static_cast<DWORD>((resolveTimeoutSeconds > 0 ? resolveTimeoutSeconds : 5) * 1000);
|
|
373
378
|
browse->regType = L"_" + type + L"._" + protocol;
|
|
374
379
|
browse->domain = DomainName(domain);
|
|
375
380
|
browse->typesOnly = type == L"services._dns-sd" && protocol == L"udp";
|
|
@@ -490,7 +495,7 @@ void Zeroconf::OnBrowse(const std::shared_ptr<Browse> &browse, DWORD status, PDN
|
|
|
490
495
|
resolve->queryName = WithoutTrailingDot(record->pName);
|
|
491
496
|
resolve->interfaceIndex = browse->interfaceIndex;
|
|
492
497
|
browse->resolves[instanceKey] = resolve;
|
|
493
|
-
|
|
498
|
+
StartScanResolve(browse, resolve, after);
|
|
494
499
|
}
|
|
495
500
|
continue;
|
|
496
501
|
}
|
|
@@ -552,7 +557,7 @@ void Zeroconf::OnBrowse(const std::shared_ptr<Browse> &browse, DWORD status, PDN
|
|
|
552
557
|
resolve->queryName = target;
|
|
553
558
|
resolve->interfaceIndex = browse->interfaceIndex;
|
|
554
559
|
browse->resolves[key] = resolve;
|
|
555
|
-
|
|
560
|
+
StartScanResolve(browse, resolve, after);
|
|
556
561
|
}
|
|
557
562
|
}
|
|
558
563
|
}
|
|
@@ -590,6 +595,54 @@ void Zeroconf::StartResolve(const std::shared_ptr<Resolve> &resolve, After &afte
|
|
|
590
595
|
});
|
|
591
596
|
}
|
|
592
597
|
|
|
598
|
+
// Resolves of a scan time out after the scan's resolveTimeout, are retried once, then reported with a TIMEOUT error
|
|
599
|
+
void Zeroconf::StartScanResolve(const std::shared_ptr<Browse> &browse, const std::shared_ptr<Resolve> &resolve, After &after) {
|
|
600
|
+
StartResolve(resolve, after);
|
|
601
|
+
if (resolve->done) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
std::weak_ptr<Resolve> weak = resolve;
|
|
605
|
+
auto alive = alive_;
|
|
606
|
+
DWORD timeout = browse->resolveTimeoutMs;
|
|
607
|
+
std::thread([this, weak, alive, timeout] {
|
|
608
|
+
Sleep(timeout);
|
|
609
|
+
if (!*alive) return;
|
|
610
|
+
auto expired = weak.lock();
|
|
611
|
+
if (!expired) return;
|
|
612
|
+
After timedOut;
|
|
613
|
+
{
|
|
614
|
+
std::lock_guard<std::recursive_mutex> lock(mutex_);
|
|
615
|
+
auto browse = expired->browse.lock();
|
|
616
|
+
auto current = browse ? browses_.find(browse->scanId) : browses_.end();
|
|
617
|
+
if (expired->done || current == browses_.end() || current->second != browse) return;
|
|
618
|
+
auto entry = browse->resolves.find(expired->key);
|
|
619
|
+
if (entry == browse->resolves.end() || entry->second != expired) return;
|
|
620
|
+
CancelResolve(expired, timedOut);
|
|
621
|
+
browse->resolves.erase(entry);
|
|
622
|
+
if (!expired->retried) {
|
|
623
|
+
// Slow devices can time out, retry once before reporting the error
|
|
624
|
+
auto retry = std::make_shared<Resolve>(this);
|
|
625
|
+
retry->browse = browse;
|
|
626
|
+
retry->scanId = expired->scanId;
|
|
627
|
+
retry->name = expired->name;
|
|
628
|
+
retry->key = expired->key;
|
|
629
|
+
retry->queryName = expired->queryName;
|
|
630
|
+
retry->interfaceIndex = expired->interfaceIndex;
|
|
631
|
+
retry->retried = true;
|
|
632
|
+
browse->resolves[retry->key] = retry;
|
|
633
|
+
StartScanResolve(browse, retry, timedOut);
|
|
634
|
+
} else {
|
|
635
|
+
Error error = LibraryError(L"TIMEOUT", L"Resolving service " + expired->name + L" failed: timed out", expired->name);
|
|
636
|
+
std::string scanId = expired->scanId;
|
|
637
|
+
timedOut.push_back([this, scanId, error] {
|
|
638
|
+
if (events_.error) events_.error(scanId, error);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
Run(timedOut);
|
|
643
|
+
}).detach();
|
|
644
|
+
}
|
|
645
|
+
|
|
593
646
|
void Zeroconf::CancelResolve(const std::shared_ptr<Resolve> &resolve, After &after) {
|
|
594
647
|
if (resolve->done) {
|
|
595
648
|
return;
|
|
@@ -89,7 +89,8 @@ class Zeroconf {
|
|
|
89
89
|
const std::wstring &protocol,
|
|
90
90
|
const std::wstring &domain,
|
|
91
91
|
const std::wstring &subtype,
|
|
92
|
-
const std::wstring &networkInterface
|
|
92
|
+
const std::wstring &networkInterface,
|
|
93
|
+
double resolveTimeoutSeconds = 5);
|
|
93
94
|
void Stop(const std::string &scanId);
|
|
94
95
|
void StopAll();
|
|
95
96
|
|
|
@@ -128,6 +129,7 @@ class Zeroconf {
|
|
|
128
129
|
|
|
129
130
|
void StopLocked(const std::string &scanId, After &after);
|
|
130
131
|
void StartResolve(const std::shared_ptr<Resolve> &resolve, After &after);
|
|
132
|
+
void StartScanResolve(const std::shared_ptr<Browse> &browse, const std::shared_ptr<Resolve> &resolve, After &after);
|
|
131
133
|
void CancelResolve(const std::shared_ptr<Resolve> &resolve, After &after);
|
|
132
134
|
void PublishAs(
|
|
133
135
|
const std::wstring ®Type,
|