react-native-zeroconf 0.15.0 → 0.15.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 +16 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +10 -2
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +23 -8
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +56 -12
- package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +17 -9
- package/dist/index.js +28 -3
- package/index.d.ts +14 -4
- package/ios/RNZeroconf/RNZeroconf.h +2 -1
- package/ios/RNZeroconf/RNZeroconf.m +83 -9
- package/package.json +1 -1
- package/src/index.js +24 -5
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ Publish a service on the network.
|
|
|
162
162
|
| `domain` | string | `'local.'` | Domain |
|
|
163
163
|
| `name` | string | required | Service name (should be unique) |
|
|
164
164
|
| `port` | number | required | Port number |
|
|
165
|
-
| `txt` | object | `{}`
|
|
165
|
+
| `txt` | object \| array | `{}` | TXT record key-value pairs, or `[key, value]` pairs to control the order |
|
|
166
166
|
| `implType` | string | `'NSD'` | **Android only**: `'NSD'` or `'DNSSD'` |
|
|
167
167
|
|
|
168
168
|
```javascript
|
|
@@ -170,8 +170,18 @@ zeroconf.publishService('http', 'tcp', 'local.', 'MyWebServer', 8080, {
|
|
|
170
170
|
path: '/api',
|
|
171
171
|
version: '1.0',
|
|
172
172
|
})
|
|
173
|
+
|
|
174
|
+
// TXT records are published in the order given. Use pairs when order matters and keys look like numbers
|
|
175
|
+
zeroconf.publishService('http', 'tcp', 'local.', 'MyWebServer', 8080, [
|
|
176
|
+
['txtvers', '1'],
|
|
177
|
+
['path', '/api'],
|
|
178
|
+
])
|
|
173
179
|
```
|
|
174
180
|
|
|
181
|
+
TXT order is preserved on iOS and with `DNSSD` on Android. With `NSD`, the order depends on the Android system.
|
|
182
|
+
|
|
183
|
+
Published services are unpublished when the React Native instance is torn down (e.g. on reload). If the app process is killed, it can't announce anything: other devices only drop the service once its mDNS records expire.
|
|
184
|
+
|
|
175
185
|
#### `unpublishService(name, implType)`
|
|
176
186
|
|
|
177
187
|
Remove a published service.
|
|
@@ -235,10 +245,12 @@ useEffect(() => {
|
|
|
235
245
|
fullName: 'XeroxPrinter._http._tcp.local.', // Full service name
|
|
236
246
|
host: 'XeroxPrinter.local.', // Hostname
|
|
237
247
|
port: 8080, // Port number
|
|
238
|
-
addresses: [ // IP addresses
|
|
248
|
+
addresses: [ // IP addresses, IPv4 first
|
|
239
249
|
'192.168.1.23',
|
|
240
250
|
'fe80::aebc:123:ffff:abcd'
|
|
241
251
|
],
|
|
252
|
+
ipv4: ['192.168.1.23'], // IPv4 addresses only
|
|
253
|
+
ipv6: ['fe80::aebc:123:ffff:abcd'], // IPv6 addresses only
|
|
242
254
|
txt: { // TXT record attributes
|
|
243
255
|
path: '/status',
|
|
244
256
|
color: 'yes'
|
|
@@ -246,6 +258,8 @@ useEffect(() => {
|
|
|
246
258
|
}
|
|
247
259
|
```
|
|
248
260
|
|
|
261
|
+
On Android with `NSD`, `host` is the mDNS hostname on Android 16+. Older versions don't expose it, so `host` is usually the IP address (use `DNSSD` if you need the hostname there). `NSD` returns all addresses on Android 14+, and a single one before.
|
|
262
|
+
|
|
249
263
|
## Android Implementation Types
|
|
250
264
|
|
|
251
265
|
This library supports two implementations on Android:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package com.balthazargronon.RCTZeroconf;
|
|
2
2
|
|
|
3
|
-
import com.facebook.react.bridge.
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
4
4
|
|
|
5
5
|
public interface Zeroconf {
|
|
6
6
|
|
|
@@ -10,5 +10,13 @@ public interface Zeroconf {
|
|
|
10
10
|
|
|
11
11
|
public void unregisterService(String serviceName);
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param txt TXT records as ordered [key, value] pairs
|
|
15
|
+
*/
|
|
16
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Unregister every service published through this implementation
|
|
20
|
+
*/
|
|
21
|
+
void unregisterAllServices();
|
|
14
22
|
}
|
|
@@ -7,7 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
7
7
|
import com.facebook.react.bridge.ReactContext;
|
|
8
8
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
9
9
|
import com.facebook.react.bridge.ReactMethod;
|
|
10
|
-
import com.facebook.react.bridge.
|
|
10
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
11
11
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
12
12
|
|
|
13
13
|
import javax.annotation.Nullable;
|
|
@@ -73,7 +73,7 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
@ReactMethod
|
|
76
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
76
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, String implType) {
|
|
77
77
|
try {
|
|
78
78
|
getZeroconfImpl(implType).registerService(type, protocol, domain, name, port, txt);
|
|
79
79
|
} catch (Throwable e) {
|
|
@@ -95,21 +95,36 @@ public class ZeroconfModule extends ReactContextBaseJavaModule {
|
|
|
95
95
|
public void sendEvent(ReactContext reactContext,
|
|
96
96
|
String eventName,
|
|
97
97
|
@Nullable Object params) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
try {
|
|
99
|
+
reactContext
|
|
100
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
101
|
+
.emit(eventName, params);
|
|
102
|
+
} catch (Throwable e) {
|
|
103
|
+
// The JS side may already be gone, e.g. callbacks arriving during teardown
|
|
104
|
+
Log.w(getClass().getName(), "Could not send " + eventName + ": " + e.getMessage());
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
// Called on teardown by current React Native versions.
|
|
109
|
+
// No @Override so it still compiles against versions that don't declare it.
|
|
110
|
+
public void invalidate() {
|
|
111
|
+
teardown();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Called on teardown by older React Native versions, deprecated in favor of invalidate().
|
|
115
|
+
// No @Override so it still compiles once it is removed.
|
|
104
116
|
public void onCatalystInstanceDestroy() {
|
|
105
|
-
|
|
117
|
+
teardown();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private void teardown() {
|
|
106
121
|
try {
|
|
107
122
|
for (Zeroconf impl : zeroConfFactory.getCreatedImpls()) {
|
|
108
123
|
impl.stop();
|
|
124
|
+
impl.unregisterAllServices();
|
|
109
125
|
}
|
|
110
126
|
} catch (Throwable e) {
|
|
111
127
|
Log.e(getClass().getName(), e.getMessage(), e);
|
|
112
|
-
sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_ERROR, "Exception During Catalyst Destroy: " + e.getMessage());
|
|
113
128
|
}
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -14,8 +14,7 @@ import com.balthazargronon.RCTZeroconf.Zeroconf;
|
|
|
14
14
|
import com.balthazargronon.RCTZeroconf.ZeroconfModule;
|
|
15
15
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
16
16
|
|
|
17
|
-
import com.facebook.react.bridge.
|
|
18
|
-
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
17
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
19
18
|
import com.facebook.react.bridge.WritableArray;
|
|
20
19
|
import com.facebook.react.bridge.WritableMap;
|
|
21
20
|
import com.facebook.react.bridge.WritableNativeArray;
|
|
@@ -24,6 +23,9 @@ import com.facebook.react.bridge.WritableNativeMap;
|
|
|
24
23
|
import java.net.InetAddress;
|
|
25
24
|
import java.nio.charset.StandardCharsets;
|
|
26
25
|
import java.util.ArrayDeque;
|
|
26
|
+
import java.util.ArrayList;
|
|
27
|
+
import java.util.Collections;
|
|
28
|
+
import java.util.List;
|
|
27
29
|
import java.util.Locale;
|
|
28
30
|
import java.util.Map;
|
|
29
31
|
import java.util.concurrent.ConcurrentHashMap;
|
|
@@ -139,7 +141,7 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
@Override
|
|
142
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
144
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt) {
|
|
143
145
|
String serviceType = String.format("_%s._%s.", type, protocol);
|
|
144
146
|
|
|
145
147
|
final NsdManager nsdManager = this.getNsdManager();
|
|
@@ -148,10 +150,9 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
148
150
|
serviceInfo.setServiceType(serviceType);
|
|
149
151
|
serviceInfo.setPort(port);
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
serviceInfo.setAttribute(key, txt.getString(key));
|
|
153
|
+
for (int i = 0; i < txt.size(); i++) {
|
|
154
|
+
ReadableArray pair = txt.getArray(i);
|
|
155
|
+
serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
nsdManager.registerService(
|
|
@@ -171,6 +172,13 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
@Override
|
|
176
|
+
public void unregisterAllServices() {
|
|
177
|
+
for (String serviceName : new ArrayList<>(mPublishedServices.keySet())) {
|
|
178
|
+
unregisterService(serviceName);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
private NsdManager getNsdManager() {
|
|
175
183
|
if (mNsdManager == null) {
|
|
176
184
|
mNsdManager = (NsdManager) getReactApplicationContext().getSystemService(Context.NSD_SERVICE);
|
|
@@ -275,19 +283,55 @@ public class NsdServiceImpl implements Zeroconf {
|
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
285
|
|
|
286
|
+
/**
|
|
287
|
+
* All the addresses of the service on Android 14+, only one before
|
|
288
|
+
*/
|
|
289
|
+
@SuppressWarnings("deprecation")
|
|
290
|
+
private static List<InetAddress> getHostAddresses(NsdServiceInfo serviceInfo) {
|
|
291
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
292
|
+
return serviceInfo.getHostAddresses();
|
|
293
|
+
}
|
|
294
|
+
InetAddress host = serviceInfo.getHost();
|
|
295
|
+
return host == null ? Collections.<InetAddress>emptyList() : Collections.singletonList(host);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The mDNS hostname (e.g. "MyHost.local.") when available.
|
|
300
|
+
* NsdServiceInfo.getHostname() is API 36 (or backported through the Connectivity module),
|
|
301
|
+
* it is called through reflection so the library still compiles against older SDKs.
|
|
302
|
+
*/
|
|
303
|
+
private static String getMdnsHostname(NsdServiceInfo serviceInfo) {
|
|
304
|
+
try {
|
|
305
|
+
Object hostname = NsdServiceInfo.class.getMethod("getHostname").invoke(serviceInfo);
|
|
306
|
+
if (hostname instanceof String && !((String) hostname).isEmpty()) {
|
|
307
|
+
return hostname + ".local.";
|
|
308
|
+
}
|
|
309
|
+
} catch (Exception e) {
|
|
310
|
+
// Not available on this device
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
278
315
|
private WritableMap serviceInfoToMap(NsdServiceInfo serviceInfo) {
|
|
279
316
|
WritableMap service = new WritableNativeMap();
|
|
280
317
|
service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
|
|
281
|
-
final InetAddress
|
|
318
|
+
final List<InetAddress> hostAddresses = getHostAddresses(serviceInfo);
|
|
282
319
|
final String fullServiceName;
|
|
283
|
-
if (
|
|
320
|
+
if (hostAddresses.isEmpty()) {
|
|
284
321
|
fullServiceName = serviceInfo.getServiceName();
|
|
285
322
|
} else {
|
|
286
|
-
|
|
287
|
-
|
|
323
|
+
String hostname = getMdnsHostname(serviceInfo);
|
|
324
|
+
if (hostname == null) {
|
|
325
|
+
// No hostname API before Android 16, falls back to a reverse lookup (often the IP)
|
|
326
|
+
hostname = hostAddresses.get(0).getHostName();
|
|
327
|
+
}
|
|
328
|
+
fullServiceName = hostname + serviceInfo.getServiceType();
|
|
329
|
+
service.putString(ZeroconfModule.KEY_SERVICE_HOST, hostname);
|
|
288
330
|
|
|
289
331
|
WritableArray addresses = new WritableNativeArray();
|
|
290
|
-
|
|
332
|
+
for (InetAddress address : hostAddresses) {
|
|
333
|
+
addresses.pushString(address.getHostAddress());
|
|
334
|
+
}
|
|
291
335
|
|
|
292
336
|
service.putArray(ZeroconfModule.KEY_SERVICE_ADDRESSES, addresses);
|
|
293
337
|
}
|
|
@@ -8,8 +8,7 @@ import android.util.Log;
|
|
|
8
8
|
import com.balthazargronon.RCTZeroconf.Zeroconf;
|
|
9
9
|
import com.balthazargronon.RCTZeroconf.ZeroconfModule;
|
|
10
10
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
11
|
-
import com.facebook.react.bridge.
|
|
12
|
-
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
11
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
13
12
|
import com.facebook.react.bridge.WritableArray;
|
|
14
13
|
import com.facebook.react.bridge.WritableMap;
|
|
15
14
|
import com.facebook.react.bridge.WritableNativeArray;
|
|
@@ -19,7 +18,9 @@ import com.github.druk.rx2dnssd.Rx2Dnssd;
|
|
|
19
18
|
import com.github.druk.rx2dnssd.Rx2DnssdEmbedded;
|
|
20
19
|
|
|
21
20
|
import java.net.InetAddress;
|
|
21
|
+
import java.util.ArrayList;
|
|
22
22
|
import java.util.HashMap;
|
|
23
|
+
import java.util.LinkedHashMap;
|
|
23
24
|
import java.util.Locale;
|
|
24
25
|
import java.util.Map;
|
|
25
26
|
import java.util.List;
|
|
@@ -187,7 +188,7 @@ public class DnssdImpl implements Zeroconf {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
@Override
|
|
190
|
-
public void registerService(String type, String protocol, String domain, String name, int port,
|
|
191
|
+
public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt) {
|
|
191
192
|
BonjourService bs = new BonjourService.Builder(0, 0, name, getServiceType(type, protocol), null)
|
|
192
193
|
.port(port)
|
|
193
194
|
.dnsRecords(getTxtRecordMap(txt))
|
|
@@ -210,12 +211,19 @@ public class DnssdImpl implements Zeroconf {
|
|
|
210
211
|
mRegisteredDisposables.put(name, registerDisposable);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
@Override
|
|
215
|
+
public void unregisterAllServices() {
|
|
216
|
+
for (String serviceName : new ArrayList<>(mRegisteredDisposables.keySet())) {
|
|
217
|
+
unregisterService(serviceName);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private Map<String, String> getTxtRecordMap(ReadableArray txt) {
|
|
222
|
+
// LinkedHashMap keeps the TXT record order
|
|
223
|
+
Map<String, String> txtMap = new LinkedHashMap<>();
|
|
224
|
+
for (int i = 0; i < txt.size(); i++) {
|
|
225
|
+
ReadableArray pair = txt.getArray(i);
|
|
226
|
+
txtMap.put(pair.getString(0), pair.getString(1));
|
|
219
227
|
}
|
|
220
228
|
return txtMap;
|
|
221
229
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,30 @@ const ImplType = exports.ImplType = {
|
|
|
11
11
|
NSD: 'NSD',
|
|
12
12
|
DNSSD: 'DNSSD'
|
|
13
13
|
};
|
|
14
|
+
const isIPv6 = address => address.includes(':');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sort addresses IPv4 first and expose them by family
|
|
18
|
+
*/
|
|
19
|
+
const withAddressFamilies = service => {
|
|
20
|
+
const addresses = Array.isArray(service.addresses) ? service.addresses : [];
|
|
21
|
+
const ipv4 = addresses.filter(address => !isIPv6(address));
|
|
22
|
+
const ipv6 = addresses.filter(isIPv6);
|
|
23
|
+
return {
|
|
24
|
+
...service,
|
|
25
|
+
addresses: [...ipv4, ...ipv6],
|
|
26
|
+
ipv4,
|
|
27
|
+
ipv6
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* TXT records as ordered [key, value] pairs, from an object or an array of pairs
|
|
33
|
+
*/
|
|
34
|
+
const toTxtPairs = txt => {
|
|
35
|
+
const entries = Array.isArray(txt) ? txt : Object.entries(txt || {});
|
|
36
|
+
return entries.map(([key, value]) => [String(key), String(value)]);
|
|
37
|
+
};
|
|
14
38
|
class Zeroconf extends _events.EventEmitter {
|
|
15
39
|
constructor(props) {
|
|
16
40
|
super(props);
|
|
@@ -61,10 +85,11 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
61
85
|
this.emit('remove', name);
|
|
62
86
|
this.emit('update');
|
|
63
87
|
});
|
|
64
|
-
this._dListeners.resolved = _reactNative.DeviceEventEmitter.addListener('RNZeroconfResolved',
|
|
65
|
-
if (!
|
|
88
|
+
this._dListeners.resolved = _reactNative.DeviceEventEmitter.addListener('RNZeroconfResolved', data => {
|
|
89
|
+
if (!data || !data.name) {
|
|
66
90
|
return;
|
|
67
91
|
}
|
|
92
|
+
const service = withAddressFamilies(data);
|
|
68
93
|
this._services[service.name] = service;
|
|
69
94
|
this.emit('resolved', service);
|
|
70
95
|
this.emit('update');
|
|
@@ -135,7 +160,7 @@ class Zeroconf extends _events.EventEmitter {
|
|
|
135
160
|
* Publish a service
|
|
136
161
|
*/
|
|
137
162
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
138
|
-
const txtRecord =
|
|
163
|
+
const txtRecord = toTxtPairs(txt);
|
|
139
164
|
if (_reactNative.Platform.OS === 'android') {
|
|
140
165
|
this._publishedImplTypes[name] = implType;
|
|
141
166
|
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType);
|
package/index.d.ts
CHANGED
|
@@ -14,12 +14,22 @@ export interface Service {
|
|
|
14
14
|
/** Hostname, e.g. `XeroxPrinter.local.` */
|
|
15
15
|
host: string
|
|
16
16
|
port: number
|
|
17
|
-
/** IPv4 and
|
|
17
|
+
/** IPv4 and IPv6 addresses, IPv4 first */
|
|
18
18
|
addresses: string[]
|
|
19
|
+
ipv4: string[]
|
|
20
|
+
ipv6: string[]
|
|
19
21
|
/** TXT record attributes */
|
|
20
22
|
txt: Record<string, string>
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
type TxtValue = string | number | boolean
|
|
26
|
+
|
|
27
|
+
/** TXT records as an object, or as [key, value] pairs to control the order */
|
|
28
|
+
export type TxtRecord = Record<string, TxtValue> | Array<[string, TxtValue]>
|
|
29
|
+
|
|
30
|
+
/** Published services don't carry the address family fields */
|
|
31
|
+
export type PublishedService = Omit<Service, 'ipv4' | 'ipv6'>
|
|
32
|
+
|
|
23
33
|
export interface ZeroconfEvents {
|
|
24
34
|
start: () => void
|
|
25
35
|
stop: () => void
|
|
@@ -32,8 +42,8 @@ export interface ZeroconfEvents {
|
|
|
32
42
|
/** Services list changed */
|
|
33
43
|
update: () => void
|
|
34
44
|
error: (error: Error) => void
|
|
35
|
-
published: (service:
|
|
36
|
-
unpublished: (service:
|
|
45
|
+
published: (service: PublishedService) => void
|
|
46
|
+
unpublished: (service: PublishedService) => void
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/** Extends the `events` package EventEmitter */
|
|
@@ -79,7 +89,7 @@ export default class Zeroconf {
|
|
|
79
89
|
domain: string | undefined,
|
|
80
90
|
name: string,
|
|
81
91
|
port: number,
|
|
82
|
-
txt?:
|
|
92
|
+
txt?: TxtRecord,
|
|
83
93
|
implType?: ImplType,
|
|
84
94
|
): void
|
|
85
95
|
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
#import <React/RCTBridge.h>
|
|
11
11
|
#import <React/RCTBridgeModule.h>
|
|
12
12
|
#import <React/RCTEventDispatcher.h>
|
|
13
|
+
#import <React/RCTInvalidating.h>
|
|
13
14
|
|
|
14
|
-
@interface RNZeroconf : NSObject <RCTBridgeModule, NSNetServiceBrowserDelegate, NSNetServiceDelegate>
|
|
15
|
+
@interface RNZeroconf : NSObject <RCTBridgeModule, RCTInvalidating, NSNetServiceBrowserDelegate, NSNetServiceDelegate>
|
|
15
16
|
|
|
16
17
|
@property (nonatomic, strong) NSNetServiceBrowser *browser;
|
|
17
18
|
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
@property (nonatomic, strong, readonly) NSMutableDictionary *resolvingServices;
|
|
15
15
|
@property (nonatomic, strong, readonly) NSMutableDictionary *publishedServices;
|
|
16
|
+
// Services already retried after a resolve timeout
|
|
17
|
+
@property (nonatomic, strong, readonly) NSMutableSet<NSString *> *retriedServices;
|
|
18
|
+
// Stopped browsers, kept alive until their asynchronous stop has been processed
|
|
19
|
+
@property (nonatomic, strong, readonly) NSMutableSet<NSNetServiceBrowser *> *stoppingBrowsers;
|
|
20
|
+
@property (nonatomic, assign) NSTimeInterval resolveTimeoutSeconds;
|
|
16
21
|
|
|
17
22
|
@end
|
|
18
23
|
|
|
@@ -25,13 +30,34 @@ RCT_EXPORT_MODULE()
|
|
|
25
30
|
RCT_EXPORT_METHOD(scan:(NSString *)type protocol:(NSString *)protocol domain:(NSString *)domain)
|
|
26
31
|
{
|
|
27
32
|
[self stop];
|
|
33
|
+
|
|
34
|
+
// A fresh browser for each scan, restarting a search on a browser that is still stopping can crash (#154)
|
|
35
|
+
self.browser = [[NSNetServiceBrowser alloc] init];
|
|
36
|
+
[self.browser setDelegate:self];
|
|
28
37
|
[self.browser searchForServicesOfType:[NSString stringWithFormat:@"_%@._%@.", type, protocol] inDomain:domain];
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
RCT_EXPORT_METHOD(stop)
|
|
32
41
|
{
|
|
33
|
-
|
|
42
|
+
NSNetServiceBrowser *browser = self.browser;
|
|
43
|
+
if (browser) {
|
|
44
|
+
self.browser = nil;
|
|
45
|
+
[self.stoppingBrowsers addObject:browser];
|
|
46
|
+
[browser stop];
|
|
47
|
+
|
|
48
|
+
// The stop completes asynchronously on the run loop, release the browser well after it
|
|
49
|
+
__weak RNZeroconf *weakSelf = self;
|
|
50
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
51
|
+
browser.delegate = nil;
|
|
52
|
+
[weakSelf.stoppingBrowsers removeObject:browser];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
for (NSNetService *service in self.resolvingServices.allValues) {
|
|
56
|
+
service.delegate = nil;
|
|
57
|
+
[service stop];
|
|
58
|
+
}
|
|
34
59
|
[self.resolvingServices removeAllObjects];
|
|
60
|
+
[self.retriedServices removeAllObjects];
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
- (dispatch_queue_t)methodQueue
|
|
@@ -49,15 +75,14 @@ RCT_EXPORT_METHOD(registerService:(NSString *)type
|
|
|
49
75
|
domain:(NSString *)domain
|
|
50
76
|
name:(NSString *)name
|
|
51
77
|
port:(int)port
|
|
52
|
-
txt:(
|
|
78
|
+
txt:(NSArray<NSArray<NSString *> *> *)txt)
|
|
53
79
|
{
|
|
54
80
|
const NSNetService *svc = [[NSNetService alloc] initWithDomain:domain type:[NSString stringWithFormat:@"_%@._%@.", type, protocol] name:name port:port];
|
|
55
81
|
[svc setDelegate:self];
|
|
56
82
|
[svc scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
|
57
83
|
|
|
58
|
-
if (txt) {
|
|
59
|
-
|
|
60
|
-
[svc setTXTRecordData:txtData];
|
|
84
|
+
if (txt.count > 0) {
|
|
85
|
+
[svc setTXTRecordData:[self TXTRecordDataFromPairs:txt]];
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
[svc publish];
|
|
@@ -94,7 +119,7 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
94
119
|
self.resolvingServices[service.name] = service;
|
|
95
120
|
|
|
96
121
|
service.delegate = self;
|
|
97
|
-
[service resolveWithTimeout:
|
|
122
|
+
[service resolveWithTimeout:self.resolveTimeoutSeconds];
|
|
98
123
|
}
|
|
99
124
|
|
|
100
125
|
// When a service is removed.
|
|
@@ -128,7 +153,10 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
128
153
|
// When the search stops.
|
|
129
154
|
- (void) netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser
|
|
130
155
|
{
|
|
131
|
-
|
|
156
|
+
// A browser replaced by a newer scan stopping late shouldn't report the new scan as stopped
|
|
157
|
+
if (self.browser == nil || self.browser == browser) {
|
|
158
|
+
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfStop" body:nil];
|
|
159
|
+
}
|
|
132
160
|
}
|
|
133
161
|
|
|
134
162
|
// When the search starts.
|
|
@@ -147,12 +175,22 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
147
175
|
|
|
148
176
|
sender.delegate = nil;
|
|
149
177
|
[self.resolvingServices removeObjectForKey:sender.name];
|
|
178
|
+
[self.retriedServices removeObject:sender.name];
|
|
150
179
|
}
|
|
151
180
|
|
|
152
181
|
// When the service has failed to resolve it's network data (IP addresses, etc)
|
|
153
182
|
- (void) netService:(NSNetService *)sender
|
|
154
183
|
didNotResolve:(NSDictionary *)errorDict
|
|
155
184
|
{
|
|
185
|
+
// Slow devices can time out, retry once before reporting the error (#186)
|
|
186
|
+
NSNumber *code = errorDict[NSNetServicesErrorCode];
|
|
187
|
+
if (code.integerValue == NSNetServicesTimeoutError && ![self.retriedServices containsObject:sender.name]) {
|
|
188
|
+
[self.retriedServices addObject:sender.name];
|
|
189
|
+
[sender resolveWithTimeout:self.resolveTimeoutSeconds];
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
[self.retriedServices removeObject:sender.name];
|
|
193
|
+
|
|
156
194
|
[self reportError:errorDict];
|
|
157
195
|
|
|
158
196
|
sender.delegate = nil;
|
|
@@ -206,13 +244,49 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *) serviceName)
|
|
|
206
244
|
if (self) {
|
|
207
245
|
_resolvingServices = [[NSMutableDictionary alloc] init];
|
|
208
246
|
_publishedServices = [[NSMutableDictionary alloc] init];
|
|
209
|
-
|
|
210
|
-
[
|
|
247
|
+
_retriedServices = [[NSMutableSet alloc] init];
|
|
248
|
+
_stoppingBrowsers = [[NSMutableSet alloc] init];
|
|
249
|
+
_resolveTimeoutSeconds = 5.0;
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
return self;
|
|
214
253
|
}
|
|
215
254
|
|
|
255
|
+
// Builds the TXT record in the given order, unlike dataFromTXTRecordDictionary
|
|
256
|
+
- (NSData *) TXTRecordDataFromPairs:(NSArray<NSArray<NSString *> *> *)pairs
|
|
257
|
+
{
|
|
258
|
+
NSMutableData *data = [NSMutableData data];
|
|
259
|
+
for (NSArray<NSString *> *pair in pairs) {
|
|
260
|
+
if (pair.count != 2) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
NSData *entry = [[NSString stringWithFormat:@"%@=%@", pair[0], pair[1]] dataUsingEncoding:NSUTF8StringEncoding];
|
|
264
|
+
if (entry.length > 255) {
|
|
265
|
+
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfError" body:[NSString stringWithFormat:@"TXT record entry %@ is longer than 255 bytes", pair[0]]];
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
uint8_t length = (uint8_t)entry.length;
|
|
269
|
+
[data appendBytes:&length length:1];
|
|
270
|
+
[data appendData:entry];
|
|
271
|
+
}
|
|
272
|
+
return data;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Called when the bridge is torn down (e.g. reload): stop scanning and unpublish services
|
|
276
|
+
- (void) invalidate
|
|
277
|
+
{
|
|
278
|
+
[self stop];
|
|
279
|
+
// Delegates are unretained, detach them so stopping browsers never call a released module
|
|
280
|
+
for (NSNetServiceBrowser *browser in self.stoppingBrowsers) {
|
|
281
|
+
browser.delegate = nil;
|
|
282
|
+
}
|
|
283
|
+
for (NSNetService *service in self.publishedServices.allValues) {
|
|
284
|
+
service.delegate = nil;
|
|
285
|
+
[service stop];
|
|
286
|
+
}
|
|
287
|
+
[self.publishedServices removeAllObjects];
|
|
288
|
+
}
|
|
289
|
+
|
|
216
290
|
- (void) reportError:(NSDictionary *)errorDict
|
|
217
291
|
{
|
|
218
292
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"RNZeroconfError" body:[NSString stringWithFormat:@"%@",errorDict]];
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -8,6 +8,26 @@ export const ImplType = {
|
|
|
8
8
|
DNSSD: 'DNSSD',
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const isIPv6 = address => address.includes(':')
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sort addresses IPv4 first and expose them by family
|
|
15
|
+
*/
|
|
16
|
+
const withAddressFamilies = service => {
|
|
17
|
+
const addresses = Array.isArray(service.addresses) ? service.addresses : []
|
|
18
|
+
const ipv4 = addresses.filter(address => !isIPv6(address))
|
|
19
|
+
const ipv6 = addresses.filter(isIPv6)
|
|
20
|
+
return { ...service, addresses: [...ipv4, ...ipv6], ipv4, ipv6 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* TXT records as ordered [key, value] pairs, from an object or an array of pairs
|
|
25
|
+
*/
|
|
26
|
+
const toTxtPairs = txt => {
|
|
27
|
+
const entries = Array.isArray(txt) ? txt : Object.entries(txt || {})
|
|
28
|
+
return entries.map(([key, value]) => [String(key), String(value)])
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export default class Zeroconf extends EventEmitter {
|
|
12
32
|
constructor(props) {
|
|
13
33
|
super(props)
|
|
@@ -72,11 +92,12 @@ export default class Zeroconf extends EventEmitter {
|
|
|
72
92
|
this.emit('update')
|
|
73
93
|
})
|
|
74
94
|
|
|
75
|
-
this._dListeners.resolved = DeviceEventEmitter.addListener('RNZeroconfResolved',
|
|
76
|
-
if (!
|
|
95
|
+
this._dListeners.resolved = DeviceEventEmitter.addListener('RNZeroconfResolved', data => {
|
|
96
|
+
if (!data || !data.name) {
|
|
77
97
|
return
|
|
78
98
|
}
|
|
79
99
|
|
|
100
|
+
const service = withAddressFamilies(data)
|
|
80
101
|
this._services[service.name] = service
|
|
81
102
|
this.emit('resolved', service)
|
|
82
103
|
this.emit('update')
|
|
@@ -157,9 +178,7 @@ export default class Zeroconf extends EventEmitter {
|
|
|
157
178
|
* Publish a service
|
|
158
179
|
*/
|
|
159
180
|
publishService(type, protocol, domain = 'local.', name, port, txt = {}, implType = ImplType.NSD) {
|
|
160
|
-
const txtRecord =
|
|
161
|
-
Object.entries(txt || {}).map(([key, value]) => [key, String(value)]),
|
|
162
|
-
)
|
|
181
|
+
const txtRecord = toTxtPairs(txt)
|
|
163
182
|
if (Platform.OS === 'android') {
|
|
164
183
|
this._publishedImplTypes[name] = implType
|
|
165
184
|
RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType)
|