@devioarts/capacitor-mdns 0.0.3 → 0.2.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/CHANGELOG.md +110 -0
- package/DevioartsCapacitorMdns.podspec +3 -3
- package/Package.swift +1 -1
- package/README.md +272 -101
- package/android/build.gradle +3 -9
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/AndroidNsdBackend.kt +145 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsOptions.kt +32 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/MdnsValidation.kt +26 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/NsdBackend.kt +59 -0
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNS.kt +496 -175
- package/android/src/main/java/com/devioarts/capacitor/mdns/mDNSPlugin.kt +85 -49
- package/dist/docs.json +313 -15
- package/dist/esm/definitions.d.ts +89 -16
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.js +22 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +8 -1
- package/dist/esm/web.js +35 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +57 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +57 -4
- package/dist/plugin.js.map +1 -1
- package/docs/electron.md +91 -0
- package/docs/platform-setup.md +58 -0
- package/docs/playground.md +50 -0
- package/docs/runtime-behavior.md +80 -0
- package/electron/dist/plugin-settings.js +22 -0
- package/electron/dist/plugin-settings.js.map +1 -0
- package/electron/dist/plugin.cjs.js +774 -0
- package/electron/dist/plugin.cjs.js.map +1 -0
- package/electron/global.d.ts +2 -0
- package/electron/mdns-bridge.cjs +33 -4
- package/electron/mdns.ts +628 -107
- package/electron/rollup.config.mjs +22 -0
- package/electron/src/index.ts +2 -0
- package/electron/src/plugin-settings.ts +19 -0
- package/electron/tsconfig.json +21 -0
- package/electron/types-public/index.d.ts +47 -0
- package/ios/Sources/mDNSPlugin/mDNS.swift +481 -124
- package/ios/Sources/mDNSPlugin/mDNSPlugin.swift +85 -23
- package/ios/Tests/mDNSPluginTests/BridgeTests.swift +46 -0
- package/ios/Tests/mDNSPluginTests/MDNSTests.swift +490 -0
- package/package.json +55 -20
- package/ios/Tests/mDNSPluginTests/mDNSPluginTests.swift +0 -15
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import Network
|
|
3
|
+
@testable import mDNSPlugin
|
|
4
|
+
|
|
5
|
+
// MARK: - Fakes
|
|
6
|
+
|
|
7
|
+
/// `NetService` that never touches the network; tests drive the delegate callbacks by hand.
|
|
8
|
+
private final class FakeNetService: NetService {
|
|
9
|
+
var resolveCalls = 0
|
|
10
|
+
var publishCalls = 0
|
|
11
|
+
var stopCalls = 0
|
|
12
|
+
var fakePort = 8080
|
|
13
|
+
var fakeAddresses: [Data]?
|
|
14
|
+
var fakeTXT: Data?
|
|
15
|
+
|
|
16
|
+
override func resolve(withTimeout timeout: TimeInterval) { resolveCalls += 1 }
|
|
17
|
+
override func publish() { publishCalls += 1 }
|
|
18
|
+
override func stop() { stopCalls += 1 }
|
|
19
|
+
override var port: Int { fakePort }
|
|
20
|
+
override var addresses: [Data]? { fakeAddresses }
|
|
21
|
+
override func txtRecordData() -> Data? { fakeTXT }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private final class FakeBrowser: NetServiceBrowser {
|
|
25
|
+
var stopCalls = 0
|
|
26
|
+
override func searchForServices(ofType type: String, inDomain domainString: String) {}
|
|
27
|
+
override func stop() { stopCalls += 1 }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private final class WatchProbe: MDNSWatchDelegate {
|
|
31
|
+
var found: [[String: Any]] = []
|
|
32
|
+
var lost: [[String: Any]] = []
|
|
33
|
+
var errors: [(String, Bool)] = []
|
|
34
|
+
func mdns(_ mdns: MDNS, didFindService service: [String: Any]) { found.append(service) }
|
|
35
|
+
func mdns(_ mdns: MDNS, didLoseService service: [String: Any]) { lost.append(service) }
|
|
36
|
+
func mdns(_ mdns: MDNS, watchError type: String, message: String, watching: Bool) { errors.append((message, watching)) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private final class Rig {
|
|
40
|
+
let mdns = MDNS()
|
|
41
|
+
var services: [FakeNetService] = []
|
|
42
|
+
var browsers: [FakeBrowser] = []
|
|
43
|
+
|
|
44
|
+
init(settleMs: Int = 30, publishTimeoutMs: Int = 5000) {
|
|
45
|
+
mdns.publishTimeoutMs = publishTimeoutMs
|
|
46
|
+
mdns.makeBrowser = { [weak self] in
|
|
47
|
+
let browser = FakeBrowser()
|
|
48
|
+
self?.browsers.append(browser)
|
|
49
|
+
return browser
|
|
50
|
+
}
|
|
51
|
+
mdns.makeService = { [weak self] domain, type, name, port in
|
|
52
|
+
let s = port.map { FakeNetService(domain: domain, type: type, name: name, port: $0) }
|
|
53
|
+
?? FakeNetService(domain: domain, type: type, name: name)
|
|
54
|
+
self?.services.append(s)
|
|
55
|
+
return s
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Start a discovery on the fallback browser and record its outcome.
|
|
60
|
+
func discover(name: String? = nil, timeoutMs: Int = 2000, outcome: @escaping (Result<[[String: Any]], Error>) -> Void) {
|
|
61
|
+
mdns.discover(type: "_http._tcp.", name: name, timeoutMs: timeoutMs, useNW: false, completion: outcome)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func found(_ name: String, type: String = "_http._tcp.", domain: String = "local.") {
|
|
65
|
+
mdns.handleServiceFound(name: name, type: type, domain: domain)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func resolve(_ service: FakeNetService, host: String = "192.168.1.5") {
|
|
69
|
+
var addr = sockaddr_in()
|
|
70
|
+
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
71
|
+
addr.sin_family = sa_family_t(AF_INET)
|
|
72
|
+
inet_pton(AF_INET, host, &addr.sin_addr)
|
|
73
|
+
service.fakeAddresses = [withUnsafeBytes(of: &addr) { Data($0) }]
|
|
74
|
+
mdns.netServiceDidResolveAddress(service)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MARK: - Tests
|
|
79
|
+
|
|
80
|
+
final class MDNSTests: XCTestCase {
|
|
81
|
+
|
|
82
|
+
private func pause(_ seconds: TimeInterval) {
|
|
83
|
+
let e = expectation(description: "delay")
|
|
84
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { e.fulfill() }
|
|
85
|
+
wait(for: [e], timeout: seconds + 2)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func testBrowsingContinuesForTheEntireTimeoutAfterAnIdleResult() {
|
|
89
|
+
let rig = Rig()
|
|
90
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
91
|
+
let done = expectation(description: "full timeout")
|
|
92
|
+
rig.discover(timeoutMs: 300) { outcome = $0; done.fulfill() }
|
|
93
|
+
rig.found("First")
|
|
94
|
+
rig.resolve(rig.services[0])
|
|
95
|
+
pause(0.15)
|
|
96
|
+
XCTAssertNil(outcome)
|
|
97
|
+
rig.found("Late")
|
|
98
|
+
rig.resolve(rig.services[1])
|
|
99
|
+
wait(for: [done], timeout: 1)
|
|
100
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected success") }
|
|
101
|
+
XCTAssertEqual(services.count, 2)
|
|
102
|
+
}
|
|
103
|
+
func testNameMatchingTreatsDotsAndHyphensEqually() {
|
|
104
|
+
let rig = Rig()
|
|
105
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
106
|
+
rig.discover(name: "My-Device") { outcome = $0 }
|
|
107
|
+
rig.found("My.Device")
|
|
108
|
+
rig.resolve(rig.services[0])
|
|
109
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected early match") }
|
|
110
|
+
XCTAssertEqual(services.first?["name"] as? String, "My.Device")
|
|
111
|
+
}
|
|
112
|
+
func testValidationEnforcesDnsAndUtf8LimitsWithoutStoppingPublisher() {
|
|
113
|
+
let rig = Rig()
|
|
114
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "Good", port: 80, txt: nil) { _ in }
|
|
115
|
+
for type in ["_123._tcp.", "_ab--cd._tcp.", "_abcdefghijklmnop._tcp.", "http"] {
|
|
116
|
+
XCTAssertThrowsError(try MDNS.validatedType(type))
|
|
117
|
+
}
|
|
118
|
+
XCTAssertEqual(try? MDNS.validatedType(" _HTTP._TCP "), "_http._tcp.")
|
|
119
|
+
XCTAssertThrowsError(try MDNS.validatedName(String(repeating: "é", count: 32)))
|
|
120
|
+
for txt in [["": "x"], ["a=b": "x"], ["a": String(repeating: "é", count: 127)], ["A": "", "a": ""], Dictionary(uniqueKeysWithValues: (0..<6).map { ("k\($0)", String(repeating: "x", count: 250)) })] {
|
|
121
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "Bad", port: 81, txt: txt) { result in
|
|
122
|
+
guard case .failure = result else { return XCTFail("invalid TXT accepted") }
|
|
123
|
+
}
|
|
124
|
+
XCTAssertEqual(rig.services[0].stopCalls, 0)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// MARK: Discovery
|
|
128
|
+
|
|
129
|
+
func testSlowResolveDoesNotFinishBeforeTheServiceIsResolved() {
|
|
130
|
+
let rig = Rig(settleMs: 30)
|
|
131
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
132
|
+
let done = expectation(description: "discovery")
|
|
133
|
+
rig.discover { outcome = $0; done.fulfill() }
|
|
134
|
+
|
|
135
|
+
rig.found("A")
|
|
136
|
+
pause(0.15) // several debounce windows pass while the resolver is still outstanding
|
|
137
|
+
XCTAssertNil(outcome, "must not settle while a resolver is pending")
|
|
138
|
+
|
|
139
|
+
rig.resolve(rig.services[0])
|
|
140
|
+
wait(for: [done], timeout: 2)
|
|
141
|
+
|
|
142
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected success") }
|
|
143
|
+
XCTAssertEqual(services.count, 1)
|
|
144
|
+
XCTAssertEqual(services.first?["name"] as? String, "A")
|
|
145
|
+
XCTAssertEqual(services.first?["port"] as? Int, 8080)
|
|
146
|
+
XCTAssertEqual(services.first?["hosts"] as? [String], ["192.168.1.5"])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func testRepeatedBrowseResultsDoNotSpawnDuplicateResolvers() {
|
|
150
|
+
let rig = Rig()
|
|
151
|
+
rig.discover { _ in }
|
|
152
|
+
|
|
153
|
+
// NWBrowser redelivers its full result set on every change, with un-normalized type/domain.
|
|
154
|
+
rig.found("A", type: "_http._tcp", domain: "local")
|
|
155
|
+
rig.found("A", type: "_http._tcp", domain: "local")
|
|
156
|
+
rig.found("A", type: "_http._tcp.", domain: "local.")
|
|
157
|
+
rig.found("A", type: "_http._tcp", domain: "")
|
|
158
|
+
|
|
159
|
+
XCTAssertEqual(rig.services.count, 1)
|
|
160
|
+
XCTAssertEqual(rig.services[0].resolveCalls, 1)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func testResolvedServiceWithoutAddressesKeepsWaitingForAddresses() {
|
|
164
|
+
let rig = Rig()
|
|
165
|
+
let done = expectation(description: "discovery")
|
|
166
|
+
rig.discover(name: "A", timeoutMs: 80) { outcome in
|
|
167
|
+
guard case .success(let services) = outcome else {
|
|
168
|
+
done.fulfill()
|
|
169
|
+
return XCTFail("expected success")
|
|
170
|
+
}
|
|
171
|
+
XCTAssertTrue(services.isEmpty)
|
|
172
|
+
done.fulfill()
|
|
173
|
+
}
|
|
174
|
+
rig.found("A")
|
|
175
|
+
rig.mdns.netServiceDidResolveAddress(rig.services[0])
|
|
176
|
+
wait(for: [done], timeout: 2)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func testTargetNameFiltersCandidatesAndFinishesEarly() {
|
|
180
|
+
let rig = Rig()
|
|
181
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
182
|
+
let done = expectation(description: "discovery")
|
|
183
|
+
rig.discover(name: "Foo", timeoutMs: 10_000) { outcome = $0; done.fulfill() }
|
|
184
|
+
|
|
185
|
+
rig.found("Other")
|
|
186
|
+
XCTAssertEqual(rig.services.count, 0, "non-matching candidates must not be resolved")
|
|
187
|
+
|
|
188
|
+
rig.found("Foo (2)")
|
|
189
|
+
XCTAssertEqual(rig.services.count, 1)
|
|
190
|
+
rig.resolve(rig.services[0])
|
|
191
|
+
wait(for: [done], timeout: 1) // early exit, long before the 10s hard timeout
|
|
192
|
+
|
|
193
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected success") }
|
|
194
|
+
XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Foo (2)"])
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func testHardTimeoutReturnsOnlyResolvedServices() {
|
|
198
|
+
let rig = Rig()
|
|
199
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
200
|
+
let done = expectation(description: "discovery")
|
|
201
|
+
rig.discover(timeoutMs: 80) { outcome = $0; done.fulfill() }
|
|
202
|
+
|
|
203
|
+
rig.found("NeverResolves")
|
|
204
|
+
wait(for: [done], timeout: 2)
|
|
205
|
+
|
|
206
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected success") }
|
|
207
|
+
XCTAssertTrue(services.isEmpty)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func testUnresolvableServiceDoesNotBlockSettling() {
|
|
211
|
+
let rig = Rig(settleMs: 30)
|
|
212
|
+
var outcome: Result<[[String: Any]], Error>?
|
|
213
|
+
let done = expectation(description: "discovery")
|
|
214
|
+
rig.discover { outcome = $0; done.fulfill() }
|
|
215
|
+
|
|
216
|
+
rig.found("Bad")
|
|
217
|
+
rig.found("Good")
|
|
218
|
+
rig.mdns.netService(rig.services[0], didNotResolve: [:])
|
|
219
|
+
rig.resolve(rig.services[1])
|
|
220
|
+
wait(for: [done], timeout: 2)
|
|
221
|
+
|
|
222
|
+
guard case .success(let services)? = outcome else { return XCTFail("expected success") }
|
|
223
|
+
XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Good"])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func testNewDiscoveryReplacesTheOldOneAndIgnoresItsStaleCallbacks() {
|
|
227
|
+
let rig = Rig()
|
|
228
|
+
var first: Result<[[String: Any]], Error>?
|
|
229
|
+
var second: Result<[[String: Any]], Error>?
|
|
230
|
+
rig.discover { first = $0 }
|
|
231
|
+
rig.found("Old")
|
|
232
|
+
let stale = rig.services[0]
|
|
233
|
+
|
|
234
|
+
rig.discover { second = $0 }
|
|
235
|
+
guard case .failure(let err)? = first else { return XCTFail("first discovery must fail as replaced") }
|
|
236
|
+
XCTAssertTrue(err.localizedDescription.contains("replaced"))
|
|
237
|
+
|
|
238
|
+
rig.resolve(stale) // callback from the previous session
|
|
239
|
+
pause(0.1)
|
|
240
|
+
XCTAssertNil(second, "a stale resolver must not complete the new discovery")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// MARK: Publishing
|
|
244
|
+
|
|
245
|
+
func testPublishSuccessReportsServiceName() {
|
|
246
|
+
let rig = Rig()
|
|
247
|
+
var outcome: Result<String, Error>?
|
|
248
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "Svc", port: 8080, txt: ["k": "v"]) { outcome = $0 }
|
|
249
|
+
|
|
250
|
+
let svc = rig.services[0]
|
|
251
|
+
XCTAssertEqual(svc.publishCalls, 1)
|
|
252
|
+
rig.mdns.netServiceDidPublish(svc)
|
|
253
|
+
|
|
254
|
+
guard case .success(let name)? = outcome else { return XCTFail("expected success") }
|
|
255
|
+
XCTAssertEqual(name, "Svc")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func testPublishTimeoutFailsAndStopsThePublisher() {
|
|
259
|
+
let rig = Rig(publishTimeoutMs: 50)
|
|
260
|
+
let done = expectation(description: "publish")
|
|
261
|
+
var outcome: Result<String, Error>?
|
|
262
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "Svc", port: 8080, txt: nil) { outcome = $0; done.fulfill() }
|
|
263
|
+
wait(for: [done], timeout: 2)
|
|
264
|
+
|
|
265
|
+
guard case .failure(let err)? = outcome else { return XCTFail("expected failure") }
|
|
266
|
+
XCTAssertTrue(err.localizedDescription.contains("Timed out"))
|
|
267
|
+
XCTAssertEqual(rig.services[0].stopCalls, 1)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func testNewBroadcastFailsThePreviousPendingOne() {
|
|
271
|
+
let rig = Rig()
|
|
272
|
+
var first: Result<String, Error>?
|
|
273
|
+
var second: Result<String, Error>?
|
|
274
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { first = $0 }
|
|
275
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "B", port: 2, txt: nil) { second = $0 }
|
|
276
|
+
|
|
277
|
+
guard case .failure(let err)? = first else { return XCTFail("first broadcast must fail as replaced") }
|
|
278
|
+
XCTAssertTrue(err.localizedDescription.contains("Replaced"))
|
|
279
|
+
XCTAssertNil(second)
|
|
280
|
+
XCTAssertEqual(rig.services[0].stopCalls, 1)
|
|
281
|
+
|
|
282
|
+
rig.mdns.netServiceDidPublish(rig.services[0]) // stale publisher: ignored
|
|
283
|
+
XCTAssertNil(second)
|
|
284
|
+
rig.mdns.netServiceDidPublish(rig.services[1])
|
|
285
|
+
guard case .success(let name)? = second else { return XCTFail("expected success") }
|
|
286
|
+
XCTAssertEqual(name, "B")
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func testDidNotPublishReportsErrorAndAllowsRepublishing() {
|
|
290
|
+
let rig = Rig()
|
|
291
|
+
var first: Result<String, Error>?
|
|
292
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { first = $0 }
|
|
293
|
+
rig.mdns.netService(rig.services[0], didNotPublish: [NetService.errorCode: NSNumber(value: -72001)])
|
|
294
|
+
|
|
295
|
+
guard case .failure(let err)? = first else { return XCTFail("expected failure") }
|
|
296
|
+
XCTAssertEqual((err as NSError).code, -72001)
|
|
297
|
+
|
|
298
|
+
var second: Result<String, Error>?
|
|
299
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { second = $0 }
|
|
300
|
+
rig.mdns.netServiceDidPublish(rig.services[1])
|
|
301
|
+
guard case .success? = second else { return XCTFail("expected success") }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func testStopBroadcastIsIdempotentAndFailsAPendingPublish() throws {
|
|
305
|
+
let rig = Rig()
|
|
306
|
+
var outcome: Result<String, Error>?
|
|
307
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: 1, txt: nil) { outcome = $0 }
|
|
308
|
+
|
|
309
|
+
try rig.mdns.stopBroadcast()
|
|
310
|
+
try rig.mdns.stopBroadcast()
|
|
311
|
+
|
|
312
|
+
guard case .failure? = outcome else { return XCTFail("pending publish must be failed by stop") }
|
|
313
|
+
XCTAssertEqual(rig.services[0].stopCalls, 1)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func testInvalidPortFailsImmediately() {
|
|
317
|
+
let rig = Rig()
|
|
318
|
+
for port in [0, -1, 65536] {
|
|
319
|
+
var outcome: Result<String, Error>?
|
|
320
|
+
rig.mdns.broadcast(type: "_http._tcp.", name: "A", port: port, txt: nil) { outcome = $0 }
|
|
321
|
+
guard case .failure? = outcome else { return XCTFail("port \(port) must be rejected") }
|
|
322
|
+
}
|
|
323
|
+
XCTAssertTrue(rig.services.isEmpty)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// MARK: Helpers
|
|
327
|
+
|
|
328
|
+
func testParseHostsHandlesIPv4AndIPv6() {
|
|
329
|
+
var v4 = sockaddr_in()
|
|
330
|
+
v4.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
331
|
+
v4.sin_family = sa_family_t(AF_INET)
|
|
332
|
+
inet_pton(AF_INET, "10.1.2.3", &v4.sin_addr)
|
|
333
|
+
|
|
334
|
+
var v6 = sockaddr_in6()
|
|
335
|
+
v6.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
|
|
336
|
+
v6.sin6_family = sa_family_t(AF_INET6)
|
|
337
|
+
inet_pton(AF_INET6, "fe80::1", &v6.sin6_addr)
|
|
338
|
+
|
|
339
|
+
let hosts = parseHosts([withUnsafeBytes(of: &v6) { Data($0) }, withUnsafeBytes(of: &v4) { Data($0) }])
|
|
340
|
+
XCTAssertEqual(hosts, ["10.1.2.3", "fe80::1"])
|
|
341
|
+
XCTAssertEqual(parseHosts(nil), [])
|
|
342
|
+
XCTAssertEqual(parseHosts([]), [])
|
|
343
|
+
|
|
344
|
+
v6.sin6_scope_id = 7
|
|
345
|
+
XCTAssertEqual(parseHosts([withUnsafeBytes(of: &v6) { Data($0) }]), ["fe80::1%7"])
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func testLocalNetworkDeniedDetection() {
|
|
349
|
+
XCTAssertTrue(MDNS.isLocalNetworkDenied(.dns(-65570)))
|
|
350
|
+
XCTAssertFalse(MDNS.isLocalNetworkDenied(.dns(-65537)))
|
|
351
|
+
XCTAssertFalse(MDNS.isLocalNetworkDenied(.posix(.ECONNREFUSED)))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// MARK: - Continuous discovery regressions
|
|
356
|
+
|
|
357
|
+
extension MDNSTests {
|
|
358
|
+
private func watch(_ rig: Rig, _ probe: WatchProbe, types: [String] = ["_http._tcp."]) {
|
|
359
|
+
rig.mdns.watchDelegate = probe
|
|
360
|
+
rig.mdns.watchServices(types: types, useNW: false) { result in
|
|
361
|
+
guard case .success? = Optional(result) else { return XCTFail("watch failed: \(result)") }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func testWatchDeduplicatesTypesAndReturnsFullLostSnapshot() {
|
|
366
|
+
let rig = Rig()
|
|
367
|
+
let probe = WatchProbe()
|
|
368
|
+
watch(rig, probe, types: [" _http._tcp", "_http._tcp.", "_ipp._tcp."])
|
|
369
|
+
XCTAssertEqual(rig.browsers.count, 2)
|
|
370
|
+
let browser = rig.browsers[0]
|
|
371
|
+
let found = FakeNetService(domain: "local.", type: "_http._tcp.", name: "A")
|
|
372
|
+
rig.mdns.netServiceBrowser(browser, didFind: found, moreComing: false)
|
|
373
|
+
rig.mdns.netServiceBrowser(browser, didFind: found, moreComing: false)
|
|
374
|
+
XCTAssertEqual(rig.services.count, 1)
|
|
375
|
+
rig.resolve(rig.services[0])
|
|
376
|
+
XCTAssertEqual(probe.found.count, 1)
|
|
377
|
+
XCTAssertEqual(probe.found[0]["hosts"] as? [String], ["192.168.1.5"])
|
|
378
|
+
rig.mdns.netServiceBrowser(browser, didRemove: found, moreComing: false)
|
|
379
|
+
XCTAssertEqual(probe.lost.count, 1)
|
|
380
|
+
XCTAssertEqual(probe.lost[0] as NSDictionary, probe.found[0] as NSDictionary)
|
|
381
|
+
rig.mdns.stopWatching()
|
|
382
|
+
XCTAssertTrue(rig.browsers.allSatisfy { $0.stopCalls == 1 })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func testWatchLossAndStopIgnoreLateResolveAndBrowserCallbacks() {
|
|
386
|
+
let rig = Rig()
|
|
387
|
+
let probe = WatchProbe()
|
|
388
|
+
watch(rig, probe)
|
|
389
|
+
rig.mdns.handleWatchFound(name: "A", type: "_http._tcp", domain: "local")
|
|
390
|
+
let oldResolver = rig.services[0]
|
|
391
|
+
rig.mdns.handleWatchLost(name: "A", type: "_http._tcp.", domain: "local.")
|
|
392
|
+
rig.resolve(oldResolver)
|
|
393
|
+
XCTAssertTrue(probe.found.isEmpty)
|
|
394
|
+
XCTAssertTrue(probe.lost.isEmpty)
|
|
395
|
+
rig.mdns.handleWatchFound(name: "B", type: "_http._tcp.", domain: "local.")
|
|
396
|
+
let oldBrowser = rig.browsers[0]
|
|
397
|
+
rig.mdns.stopWatching()
|
|
398
|
+
watch(rig, probe)
|
|
399
|
+
rig.resolve(rig.services[1])
|
|
400
|
+
rig.mdns.netServiceBrowser(oldBrowser, didFind: oldResolver, moreComing: false)
|
|
401
|
+
rig.mdns.netServiceBrowser(oldBrowser, didNotSearch: [:])
|
|
402
|
+
XCTAssertTrue(probe.found.isEmpty)
|
|
403
|
+
XCTAssertTrue(probe.errors.isEmpty)
|
|
404
|
+
XCTAssertEqual(rig.services.count, 2)
|
|
405
|
+
rig.mdns.stopWatching()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func testWatchRetriesFailedResolveAndAllowsRediscoveryAfterExhaustion() {
|
|
409
|
+
let rig = Rig()
|
|
410
|
+
let probe = WatchProbe()
|
|
411
|
+
watch(rig, probe)
|
|
412
|
+
rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
|
|
413
|
+
for attempt in 0..<4 {
|
|
414
|
+
rig.mdns.netService(rig.services[attempt], didNotResolve: [:])
|
|
415
|
+
if attempt < 3 { pause(0.2) }
|
|
416
|
+
}
|
|
417
|
+
XCTAssertEqual(probe.errors.count, 1)
|
|
418
|
+
XCTAssertEqual(probe.errors[0].1, true)
|
|
419
|
+
rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
|
|
420
|
+
rig.resolve(rig.services[4])
|
|
421
|
+
XCTAssertEqual(probe.found.count, 1)
|
|
422
|
+
rig.mdns.stopWatching()
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
func testWatchRetriesWhenResolverNeverCallsBackAndCancelsRetryOnStop() {
|
|
426
|
+
let rig = Rig()
|
|
427
|
+
rig.mdns.resolveTimeoutSeconds = 0.03
|
|
428
|
+
watch(rig, WatchProbe())
|
|
429
|
+
rig.mdns.handleWatchFound(name: "A", type: "_http._tcp.", domain: "local.")
|
|
430
|
+
pause(0.22)
|
|
431
|
+
XCTAssertGreaterThanOrEqual(rig.services.count, 2)
|
|
432
|
+
rig.mdns.stopWatching()
|
|
433
|
+
let count = rig.services.count
|
|
434
|
+
pause(0.25)
|
|
435
|
+
XCTAssertEqual(rig.services.count, count)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func testWatchAndDiscoverStayIndependentAndInvalidReplacementKeepsWatch() {
|
|
439
|
+
let rig = Rig()
|
|
440
|
+
let probe = WatchProbe()
|
|
441
|
+
watch(rig, probe)
|
|
442
|
+
var discovery: Result<[[String: Any]], Error>?
|
|
443
|
+
rig.discover(name: "Once") { discovery = $0 }
|
|
444
|
+
rig.found("Once")
|
|
445
|
+
rig.mdns.handleWatchFound(name: "Watched", type: "_http._tcp.", domain: "local.")
|
|
446
|
+
rig.resolve(rig.services[1])
|
|
447
|
+
XCTAssertEqual(probe.found.count, 1)
|
|
448
|
+
XCTAssertNil(discovery)
|
|
449
|
+
for types in [[], [" "], ["http"]] {
|
|
450
|
+
rig.mdns.watchServices(types: types, useNW: false) { result in
|
|
451
|
+
guard case .failure = result else { return XCTFail("invalid watch accepted") }
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
XCTAssertEqual(rig.browsers[0].stopCalls, 0)
|
|
455
|
+
rig.mdns.stopWatching()
|
|
456
|
+
rig.resolve(rig.services[0])
|
|
457
|
+
pause(0.05)
|
|
458
|
+
guard case .success(let services)? = discovery else { return XCTFail("discovery failed") }
|
|
459
|
+
XCTAssertEqual(services.compactMap { $0["name"] as? String }, ["Once"])
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func testNetworkWatchSnapshotsDeduplicateAndLossCancelsPendingResolution() {
|
|
463
|
+
let rig = Rig()
|
|
464
|
+
let probe = WatchProbe()
|
|
465
|
+
watch(rig, probe)
|
|
466
|
+
let values = [BrowseService(name: "A", type: "_http._tcp", domain: "local")]
|
|
467
|
+
rig.mdns.handleWatchResults(values, type: "_http._tcp.")
|
|
468
|
+
rig.mdns.handleWatchResults(values, type: "_http._tcp.")
|
|
469
|
+
XCTAssertEqual(rig.services.count, 1)
|
|
470
|
+
rig.mdns.handleWatchResults([], type: "_http._tcp.")
|
|
471
|
+
rig.resolve(rig.services[0])
|
|
472
|
+
XCTAssertTrue(probe.found.isEmpty)
|
|
473
|
+
XCTAssertTrue(probe.lost.isEmpty)
|
|
474
|
+
rig.mdns.stopWatching()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func testDeniedNetworkPermissionStopsWatchAndReportsError() {
|
|
478
|
+
let rig = Rig()
|
|
479
|
+
let probe = WatchProbe()
|
|
480
|
+
watch(rig, probe)
|
|
481
|
+
rig.mdns.handleWatchState(.waiting(.dns(-65570)), type: "_http._tcp.")
|
|
482
|
+
XCTAssertEqual(probe.errors.count, 1)
|
|
483
|
+
XCTAssertFalse(probe.errors[0].1)
|
|
484
|
+
XCTAssertTrue(probe.errors[0].0.contains("denied"))
|
|
485
|
+
XCTAssertEqual(rig.browsers[0].stopCalls, 1)
|
|
486
|
+
rig.mdns.handleWatchFound(name: "Late", type: "_http._tcp.", domain: "local.")
|
|
487
|
+
XCTAssertTrue(rig.services.isEmpty)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devioarts/capacitor-mdns",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "mDNS plugin for CapacitorJS",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/esm/index.d.ts",
|
|
8
8
|
"unpkg": "dist/plugin.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/esm/index.d.ts",
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/plugin.cjs.js",
|
|
14
|
+
"default": "./dist/esm/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./electron": {
|
|
17
|
+
"types": "./electron/types-public/index.d.ts",
|
|
18
|
+
"require": "./electron/dist/plugin.cjs.js",
|
|
19
|
+
"default": "./electron/dist/plugin.cjs.js"
|
|
20
|
+
},
|
|
21
|
+
"./electron/settings": {
|
|
22
|
+
"types": "./electron/types-public/index.d.ts",
|
|
23
|
+
"require": "./electron/dist/plugin-settings.js",
|
|
24
|
+
"default": "./electron/dist/plugin-settings.js"
|
|
25
|
+
},
|
|
26
|
+
"./electron/mdns": {
|
|
27
|
+
"types": "./electron/types-public/index.d.ts",
|
|
28
|
+
"require": "./electron/dist/plugin.cjs.js",
|
|
29
|
+
"default": "./electron/dist/plugin.cjs.js"
|
|
30
|
+
},
|
|
31
|
+
"./electron/mdns-bridge.cjs": "./electron/mdns-bridge.cjs",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
9
34
|
"files": [
|
|
35
|
+
"CHANGELOG.md",
|
|
10
36
|
"android/src/main/",
|
|
11
37
|
"android/build.gradle",
|
|
12
38
|
"dist/",
|
|
13
39
|
"electron/",
|
|
40
|
+
"docs/*.md",
|
|
14
41
|
"ios/Sources",
|
|
15
42
|
"ios/Tests",
|
|
16
43
|
"Package.swift",
|
|
@@ -45,38 +72,46 @@
|
|
|
45
72
|
"verify:web": "npm run build",
|
|
46
73
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
47
74
|
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
48
|
-
"eslint": "eslint
|
|
75
|
+
"eslint": "eslint src electron --ext ts",
|
|
49
76
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
50
77
|
"swiftlint": "node-swiftlint",
|
|
51
78
|
"docgen": "docgen --api mDNSPlugin --output-readme README.md --output-json dist/docs.json",
|
|
52
|
-
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
53
|
-
"
|
|
79
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs && npm run build-electron",
|
|
80
|
+
"build-electron": "tsc --project electron/tsconfig.json && rollup -c electron/rollup.config.mjs && rimraf ./electron/build",
|
|
81
|
+
"clean": "rimraf ./dist ./electron/dist ./electron/build",
|
|
54
82
|
"watch": "tsc --watch",
|
|
55
|
-
"test": "
|
|
83
|
+
"test": "npm run test:e2e",
|
|
84
|
+
"test:e2e": "rimraf .tmp/e2e-build && tsc -p tsconfig.e2e.json && node --test test/e2e/*.e2e.cjs",
|
|
56
85
|
"prepublishOnly": "npm run build",
|
|
57
86
|
"release": "npm publish --access public"
|
|
58
87
|
},
|
|
59
88
|
"devDependencies": {
|
|
60
|
-
"@capacitor/android": "^8.
|
|
61
|
-
"@capacitor/core": "^8.
|
|
62
|
-
"@capacitor/docgen": "^0.3.
|
|
63
|
-
"@capacitor/ios": "^8.
|
|
89
|
+
"@capacitor/android": "^8.0.0",
|
|
90
|
+
"@capacitor/core": "^8.0.0",
|
|
91
|
+
"@capacitor/docgen": "^0.3.0",
|
|
92
|
+
"@capacitor/ios": "^8.0.0",
|
|
64
93
|
"@ionic/prettier-config": "^4.0.0",
|
|
65
94
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
66
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
67
|
-
"@typescript-eslint/parser": "^8.
|
|
68
|
-
"bonjour-service": "
|
|
69
|
-
"electron": "^
|
|
70
|
-
"eslint": "^10.
|
|
71
|
-
"prettier": "^3.
|
|
72
|
-
"prettier-plugin-java": "^2.
|
|
73
|
-
"rimraf": "^6.1.
|
|
74
|
-
"rollup": "^4.
|
|
95
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
96
|
+
"@typescript-eslint/parser": "^8.63.0",
|
|
97
|
+
"bonjour-service": "~1.4.0",
|
|
98
|
+
"electron": "^43.0.0",
|
|
99
|
+
"eslint": "^10.6.0",
|
|
100
|
+
"prettier": "^3.9.0",
|
|
101
|
+
"prettier-plugin-java": "^2.10.0",
|
|
102
|
+
"rimraf": "^6.1.0",
|
|
103
|
+
"rollup": "^4.62.0",
|
|
75
104
|
"swiftlint": "^2.0.0",
|
|
76
|
-
"typescript": "^6.0.
|
|
105
|
+
"typescript": "^6.0.0"
|
|
77
106
|
},
|
|
78
107
|
"peerDependencies": {
|
|
79
|
-
"@capacitor/core": "
|
|
108
|
+
"@capacitor/core": "^8.0.0",
|
|
109
|
+
"bonjour-service": "~1.4.0"
|
|
110
|
+
},
|
|
111
|
+
"peerDependenciesMeta": {
|
|
112
|
+
"bonjour-service": {
|
|
113
|
+
"optional": true
|
|
114
|
+
}
|
|
80
115
|
},
|
|
81
116
|
"prettier": "@ionic/prettier-config",
|
|
82
117
|
"capacitor": {
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
@testable import mDNSPlugin
|
|
3
|
-
|
|
4
|
-
class mDNSTests: XCTestCase {
|
|
5
|
-
func testEcho() {
|
|
6
|
-
// This is an example of a functional test case for a plugin.
|
|
7
|
-
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
-
|
|
9
|
-
let implementation = mDNS()
|
|
10
|
-
let value = "Hello, World!"
|
|
11
|
-
let result = implementation.echo(value)
|
|
12
|
-
|
|
13
|
-
XCTAssertEqual(value, result)
|
|
14
|
-
}
|
|
15
|
-
}
|