react-native-zeroconf 0.17.0 → 0.17.2
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 +10 -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 +85 -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,13 @@
|
|
|
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
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/react-native-zeroconf"><img src="https://img.shields.io/npm/v/react-native-zeroconf?style=flat" alt="npm version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/react-native-zeroconf"><img src="https://img.shields.io/npm/dm/react-native-zeroconf?style=flat" alt="npm downloads"></a>
|
|
8
|
+
<a href="https://github.com/balthazar/react-native-zeroconf/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/balthazar/react-native-zeroconf/ci.yml?branch=master&label=tests&style=flat" alt="tests"></a>
|
|
9
|
+
<a href="https://github.com/balthazar/react-native-zeroconf/actions/workflows/windows.yml"><img src="https://img.shields.io/github/actions/workflow/status/balthazar/react-native-zeroconf/windows.yml?branch=master&label=windows&style=flat" alt="windows"></a>
|
|
10
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/react-native-zeroconf?style=flat" alt="license"></a>
|
|
3
11
|
</p>
|
|
4
12
|
|
|
5
13
|
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 +21,7 @@ yarn add react-native-zeroconf
|
|
|
13
21
|
cd ios && pod install
|
|
14
22
|
```
|
|
15
23
|
|
|
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).
|
|
24
|
+
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
25
|
|
|
18
26
|
## Usage
|
|
19
27
|
|
|
@@ -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;
|
|
@@ -520,17 +532,38 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
520
532
|
return mNsdManager;
|
|
521
533
|
}
|
|
522
534
|
|
|
535
|
+
// Before Android 14 each NsdManager client resolves one service at a time and a resolve can't be cancelled:
|
|
536
|
+
// a service that never answers would block every later resolve. After a timeout the queue moves on to a
|
|
537
|
+
// client of its own, an NsdManager from a new Context (system services are cached per Context)
|
|
538
|
+
@Nullable private volatile NsdManager mResolveManager;
|
|
539
|
+
|
|
540
|
+
private NsdManager getResolveManager() {
|
|
541
|
+
return mResolveManager != null ? mResolveManager : getNsdManager();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private void replaceResolveManager() {
|
|
545
|
+
Context context = getReactApplicationContext();
|
|
546
|
+
Context fresh = context.createConfigurationContext(context.getResources().getConfiguration());
|
|
547
|
+
mResolveManager = (NsdManager) fresh.getSystemService(Context.NSD_SERVICE);
|
|
548
|
+
}
|
|
549
|
+
|
|
523
550
|
private ReactApplicationContext getReactApplicationContext() {
|
|
524
551
|
return reactApplicationContext;
|
|
525
552
|
}
|
|
526
553
|
|
|
527
|
-
private void enqueueResolve(
|
|
554
|
+
private void enqueueResolve(NsdScan scan, NsdServiceInfo serviceInfo) {
|
|
555
|
+
// A resolve can't be cancelled before Android 14, so there is no retry: waiting twice as long
|
|
556
|
+
// matches the total time of the other implementations, which retry once
|
|
528
557
|
synchronized (mResolveQueue) {
|
|
529
|
-
mResolveQueue.add(new PendingResolve(scanId, serviceInfo, null));
|
|
558
|
+
mResolveQueue.add(new PendingResolve(scan.scanId, serviceInfo, null, scan.resolveTimeoutSeconds * 2));
|
|
530
559
|
}
|
|
531
560
|
resolveNext();
|
|
532
561
|
}
|
|
533
562
|
|
|
563
|
+
private void sendResolveTimeout(String serviceName, String scanId) {
|
|
564
|
+
zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scanId);
|
|
565
|
+
}
|
|
566
|
+
|
|
534
567
|
private void resolveNext() {
|
|
535
568
|
PendingResolve next;
|
|
536
569
|
synchronized (mResolveQueue) {
|
|
@@ -540,8 +573,11 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
540
573
|
next = mResolveQueue.poll();
|
|
541
574
|
mIsResolving = true;
|
|
542
575
|
}
|
|
576
|
+
final ZeroResolveListener listener = new ZeroResolveListener(next);
|
|
543
577
|
try {
|
|
544
|
-
|
|
578
|
+
getResolveManager().resolveService(next.serviceInfo, listener);
|
|
579
|
+
// The queue waits for the answer, or moves on to another client after the timeout
|
|
580
|
+
mHandler.postDelayed(listener::timedOut, (long) (next.timeoutSeconds * 1000));
|
|
545
581
|
} catch (Throwable e) {
|
|
546
582
|
Log.e(TAG, "resolveService failed", e);
|
|
547
583
|
String serviceName = next.serviceInfo.getServiceName();
|
|
@@ -558,25 +594,58 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
558
594
|
}
|
|
559
595
|
|
|
560
596
|
private class ZeroResolveListener implements NsdManager.ResolveListener {
|
|
597
|
+
private final PendingResolve pending;
|
|
561
598
|
@Nullable private final String scanId;
|
|
562
599
|
@Nullable private final Promise promise;
|
|
600
|
+
// Settled: answered, failed or timed out. A late answer after the timeout is ignored
|
|
601
|
+
private boolean settled = false;
|
|
563
602
|
|
|
564
|
-
ZeroResolveListener(
|
|
565
|
-
this.
|
|
566
|
-
this.
|
|
603
|
+
ZeroResolveListener(PendingResolve pending) {
|
|
604
|
+
this.pending = pending;
|
|
605
|
+
this.scanId = pending.scanId;
|
|
606
|
+
this.promise = pending.promise;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private synchronized boolean settle() {
|
|
610
|
+
if (settled) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
settled = true;
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
void timedOut() {
|
|
618
|
+
if (!settle()) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
String serviceName = pending.serviceInfo.getServiceName();
|
|
622
|
+
if (promise != null) {
|
|
623
|
+
ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName);
|
|
624
|
+
} else if (mScans.containsKey(scanId)) {
|
|
625
|
+
sendResolveTimeout(serviceName, scanId);
|
|
626
|
+
}
|
|
627
|
+
replaceResolveManager();
|
|
628
|
+
onResolveDone();
|
|
567
629
|
}
|
|
568
630
|
|
|
569
631
|
@Override
|
|
570
632
|
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
571
633
|
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
|
|
572
634
|
// Another resolve (possibly from another app or listener) is in flight: retry shortly
|
|
635
|
+
if (!settle()) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
573
638
|
synchronized (mResolveQueue) {
|
|
574
|
-
mResolveQueue.addFirst(
|
|
639
|
+
mResolveQueue.addFirst(pending);
|
|
575
640
|
mIsResolving = false;
|
|
576
641
|
}
|
|
577
642
|
mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
|
|
578
643
|
return;
|
|
579
644
|
}
|
|
645
|
+
if (!settle()) {
|
|
646
|
+
// Timed out: the queue already moved on to another client
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
580
649
|
String message = "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode);
|
|
581
650
|
if (promise != null) {
|
|
582
651
|
ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName());
|
|
@@ -588,6 +657,10 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
588
657
|
|
|
589
658
|
@Override
|
|
590
659
|
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
|
660
|
+
if (!settle()) {
|
|
661
|
+
// Timed out: the queue already moved on to another client
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
591
664
|
if (promise != null) {
|
|
592
665
|
promise.resolve(serviceInfoToMap(serviceInfo));
|
|
593
666
|
} 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,
|