@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,774 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Bonjour = require('bonjour-service');
|
|
4
|
+
var electron = require('electron');
|
|
5
|
+
var node_path = require('node:path');
|
|
6
|
+
var node_url = require('node:url');
|
|
7
|
+
var node_events = require('node:events');
|
|
8
|
+
var node_net = require('node:net');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_DISCOVER_TIMEOUT_MS = 3000;
|
|
11
|
+
/** `setTimeout` treats delays above 2^31-1 as 1ms, so clamp. */
|
|
12
|
+
const MAX_TIMER_MS = 2147483647;
|
|
13
|
+
/**
|
|
14
|
+
* Electron main-process implementation of the mDNS/Bonjour functionality.
|
|
15
|
+
*
|
|
16
|
+
* Uses the `bonjour-service` package to:
|
|
17
|
+
* - publish (advertise) a single service;
|
|
18
|
+
* - discover services of a given type and normalize results to the shared schema;
|
|
19
|
+
* - wire the API over Electron's ipcMain through init().
|
|
20
|
+
*
|
|
21
|
+
* Notes:
|
|
22
|
+
* - Instance name matching is normalized and prefix-safe, mirroring iOS/Android behavior.
|
|
23
|
+
* - TXT records are forwarded as strings when present.
|
|
24
|
+
*/
|
|
25
|
+
class mDNS extends node_events.EventEmitter {
|
|
26
|
+
constructor(bonjour, options = {}) {
|
|
27
|
+
var _a, _b, _c;
|
|
28
|
+
super();
|
|
29
|
+
this.broadcastQueue = Promise.resolve();
|
|
30
|
+
this.destroyed = false;
|
|
31
|
+
/** Settle callbacks of in-flight discoveries, so destroy() can end them instead of leaving them waiting. */
|
|
32
|
+
this.activeDiscoveries = new Set();
|
|
33
|
+
this.stoppedBrowsers = new Set();
|
|
34
|
+
this.deactivateBrowser = new Map();
|
|
35
|
+
//<editor-fold desc="Init/Destroy">
|
|
36
|
+
this.ipcRegistered = false;
|
|
37
|
+
this.onNetworkError = (err) => {
|
|
38
|
+
var _a, _b, _c;
|
|
39
|
+
this.networkError = this.toErr(err);
|
|
40
|
+
(_a = this.pendingPublishError) === null || _a === void 0 ? void 0 : _a.call(this, err);
|
|
41
|
+
const type = (_c = (_b = this.watch) === null || _b === void 0 ? void 0 : _b.types[0]) !== null && _c !== void 0 ? _c : '';
|
|
42
|
+
const watching = !!this.watch;
|
|
43
|
+
void this.stopWatching();
|
|
44
|
+
for (const abort of [...this.activeDiscoveries])
|
|
45
|
+
abort(this.toErr(err));
|
|
46
|
+
if (watching)
|
|
47
|
+
this.notify('watchError', { type, errorMessage: this.toErr(err), watching: false });
|
|
48
|
+
else
|
|
49
|
+
console.warn('[mDNS]', err);
|
|
50
|
+
};
|
|
51
|
+
this.trustedOrigins = new Set((_a = options.trustedOrigins) !== null && _a !== void 0 ? _a : []);
|
|
52
|
+
if (bonjour)
|
|
53
|
+
this.attachBonjour(bonjour);
|
|
54
|
+
this.publishTimeoutMs = (_b = options.publishTimeoutMs) !== null && _b !== void 0 ? _b : 5000;
|
|
55
|
+
this.stopTimeoutMs = (_c = options.stopTimeoutMs) !== null && _c !== void 0 ? _c : 3000;
|
|
56
|
+
}
|
|
57
|
+
attachBonjour(bonjour) {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
this.bonjourInstance = bonjour;
|
|
60
|
+
// shortcut: Bonjour 1.4 needs its internal transport for errors/TTL; recheck these tests on dependency upgrades.
|
|
61
|
+
this.transport = (_a = bonjour.server) === null || _a === void 0 ? void 0 : _a.mdns;
|
|
62
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.on('error', this.onNetworkError);
|
|
63
|
+
return bonjour;
|
|
64
|
+
}
|
|
65
|
+
get bonjour() {
|
|
66
|
+
var _a;
|
|
67
|
+
return ((_a = this.bonjourInstance) !== null && _a !== void 0 ? _a : this.attachBonjour(new Bonjour({}, (err) => console.warn('[mDNS] query response failed:', err))));
|
|
68
|
+
}
|
|
69
|
+
trustedUrl(raw) {
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(raw);
|
|
72
|
+
// Capacitor Electron's default dev renderer; packaged apps require an explicit origin.
|
|
73
|
+
if ((electron.app === null || electron.app === void 0 ? void 0 : electron.app.isPackaged) === false && `${url.protocol}//${url.host}` === 'http://localhost:5173')
|
|
74
|
+
return true;
|
|
75
|
+
if (url.protocol === 'capacitor-electron:' &&
|
|
76
|
+
url.hostname === 'localhost' &&
|
|
77
|
+
!url.username &&
|
|
78
|
+
!url.password &&
|
|
79
|
+
!url.port)
|
|
80
|
+
return true;
|
|
81
|
+
if (url.protocol === 'file:') {
|
|
82
|
+
const path = node_path.relative(electron.app.getAppPath(), node_url.fileURLToPath(url));
|
|
83
|
+
return path !== '..' && !path.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(path);
|
|
84
|
+
}
|
|
85
|
+
return this.trustedOrigins.has(`${url.protocol}//${url.host}`);
|
|
86
|
+
}
|
|
87
|
+
catch (_a) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
trustedSender(event) {
|
|
92
|
+
return (!!event.senderFrame && event.senderFrame === event.sender.mainFrame && this.trustedUrl(event.senderFrame.url));
|
|
93
|
+
}
|
|
94
|
+
registerIpc() {
|
|
95
|
+
if (this.ipcRegistered)
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
electron.ipcMain.removeHandler('mdns:startBroadcast');
|
|
99
|
+
electron.ipcMain.removeHandler('mdns:stopBroadcast');
|
|
100
|
+
electron.ipcMain.removeHandler('mdns:discover');
|
|
101
|
+
electron.ipcMain.removeHandler('mdns:getPluginPlatform');
|
|
102
|
+
electron.ipcMain.removeHandler('mdns:watchServices');
|
|
103
|
+
electron.ipcMain.removeHandler('mdns:stopWatching');
|
|
104
|
+
}
|
|
105
|
+
catch (_a) {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
const handle = (channel, action) => {
|
|
109
|
+
electron.ipcMain.handle(channel, (event, options) => {
|
|
110
|
+
if (!this.trustedSender(event))
|
|
111
|
+
throw new Error('Untrusted mDNS IPC sender');
|
|
112
|
+
return action(options);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
handle('mdns:startBroadcast', (o) => this.startBroadcast(o));
|
|
116
|
+
handle('mdns:stopBroadcast', () => this.stopBroadcast());
|
|
117
|
+
handle('mdns:discover', (o) => this.discover(o));
|
|
118
|
+
handle('mdns:getPluginPlatform', () => this.getPluginPlatform());
|
|
119
|
+
handle('mdns:watchServices', (o) => this.watchServices(o));
|
|
120
|
+
handle('mdns:stopWatching', () => this.stopWatching());
|
|
121
|
+
this.ipcRegistered = true;
|
|
122
|
+
}
|
|
123
|
+
unregisterIpc() {
|
|
124
|
+
if (!this.ipcRegistered)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
electron.ipcMain.removeHandler('mdns:startBroadcast');
|
|
128
|
+
electron.ipcMain.removeHandler('mdns:stopBroadcast');
|
|
129
|
+
electron.ipcMain.removeHandler('mdns:discover');
|
|
130
|
+
electron.ipcMain.removeHandler('mdns:getPluginPlatform');
|
|
131
|
+
electron.ipcMain.removeHandler('mdns:watchServices');
|
|
132
|
+
electron.ipcMain.removeHandler('mdns:stopWatching');
|
|
133
|
+
}
|
|
134
|
+
catch (_a) {
|
|
135
|
+
/* ignore */
|
|
136
|
+
}
|
|
137
|
+
this.ipcRegistered = false;
|
|
138
|
+
}
|
|
139
|
+
/** Register manual IPC handlers. Idempotent. */
|
|
140
|
+
init() {
|
|
141
|
+
this.registerIpc();
|
|
142
|
+
}
|
|
143
|
+
async destroy() {
|
|
144
|
+
var _a;
|
|
145
|
+
this.destroyed = true;
|
|
146
|
+
(_a = this.pendingPublishError) === null || _a === void 0 ? void 0 : _a.call(this, new Error('mDNS instance destroyed during publish'));
|
|
147
|
+
await this.stopWatching();
|
|
148
|
+
this.unregisterIpc();
|
|
149
|
+
for (const abort of [...this.activeDiscoveries])
|
|
150
|
+
abort('mDNS instance destroyed during discovery');
|
|
151
|
+
await this.withBroadcastLock(async () => {
|
|
152
|
+
var _a, _b;
|
|
153
|
+
await this.stopBroadcastUnlocked();
|
|
154
|
+
try {
|
|
155
|
+
(_a = this.bonjourInstance) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
156
|
+
(_b = this.transport) === null || _b === void 0 ? void 0 : _b.removeListener('error', this.onNetworkError);
|
|
157
|
+
this.advertiser = undefined;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn('[mDNS] bonjour.destroy error:', err);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ----------------------------- utils -----------------------------
|
|
165
|
+
toErr(err) {
|
|
166
|
+
return err instanceof Error ? err.message : String(err);
|
|
167
|
+
}
|
|
168
|
+
validatePort(port) {
|
|
169
|
+
return Number.isInteger(port) && typeof port === 'number' && port > 0 && port <= 65535
|
|
170
|
+
? null
|
|
171
|
+
: 'Missing/invalid port';
|
|
172
|
+
}
|
|
173
|
+
isRecord(value) {
|
|
174
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
175
|
+
}
|
|
176
|
+
withBroadcastLock(work) {
|
|
177
|
+
// Publish and stop share one Bonjour service; serializing them prevents orphaned advertisers.
|
|
178
|
+
const run = this.broadcastQueue.catch(() => undefined).then(work);
|
|
179
|
+
this.broadcastQueue = run.catch(() => undefined);
|
|
180
|
+
return run;
|
|
181
|
+
}
|
|
182
|
+
normalize(name) {
|
|
183
|
+
return name.replace(/ \(\d+\)$/, '').replace(/\./g, '-');
|
|
184
|
+
}
|
|
185
|
+
matchesTarget(candidate, target) {
|
|
186
|
+
if (!target)
|
|
187
|
+
return true;
|
|
188
|
+
const c = this.normalize(candidate), t = this.normalize(target);
|
|
189
|
+
return c === t || c.startsWith(t);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Parse a `_name._tcp.` style type. Absent/blank input means the default `_http._tcp.`;
|
|
193
|
+
* anything malformed is rejected (null) rather than silently advertised as something else.
|
|
194
|
+
*/
|
|
195
|
+
parseType(typeWithDot) {
|
|
196
|
+
if (typeWithDot != null && typeof typeWithDot !== 'string')
|
|
197
|
+
return null;
|
|
198
|
+
const raw = typeof typeWithDot === 'string' && typeWithDot.trim() ? typeWithDot.trim() : '_http._tcp.';
|
|
199
|
+
const m = /^_([a-z0-9](?:[a-z0-9-]{0,13}[a-z0-9])?)\._(tcp|udp)\.?$/i.exec(raw);
|
|
200
|
+
return m && /[a-z]/i.test(m[1]) && !m[1].includes('--')
|
|
201
|
+
? { type: m[1].toLowerCase(), protocol: m[2].toLowerCase() }
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
validateName(name) {
|
|
205
|
+
return name != null &&
|
|
206
|
+
(typeof name !== 'string' || Buffer.byteLength(name.trim(), 'utf8') > 63 || /[\x00-\x1f\x7f]/.test(name))
|
|
207
|
+
? 'Invalid instance name (maximum 63 UTF-8 bytes, no control characters)'
|
|
208
|
+
: null;
|
|
209
|
+
}
|
|
210
|
+
validateTxt(txt) {
|
|
211
|
+
if (txt == null)
|
|
212
|
+
return null;
|
|
213
|
+
if (!this.isRecord(txt))
|
|
214
|
+
return 'TXT must be an object of strings';
|
|
215
|
+
const keys = new Set();
|
|
216
|
+
let totalBytes = 0;
|
|
217
|
+
for (const [key, value] of Object.entries(txt)) {
|
|
218
|
+
if (!/^[\x20-\x7e]+$/.test(key) || key.includes('=') || keys.has(key.toLowerCase()))
|
|
219
|
+
return 'Invalid or duplicate TXT key';
|
|
220
|
+
if (typeof value !== 'string' || Buffer.byteLength(`${key}=${value}`, 'utf8') > 255)
|
|
221
|
+
return 'TXT entries must be strings of at most 255 UTF-8 bytes including key=';
|
|
222
|
+
totalBytes += Buffer.byteLength(`${key}=${value}`, 'utf8') + 1;
|
|
223
|
+
if (totalBytes > 1300)
|
|
224
|
+
return 'TXT record must fit in 1300 bytes including length prefixes';
|
|
225
|
+
keys.add(key.toLowerCase());
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
invalidTypeMessage(typeWithDot) {
|
|
230
|
+
return `Invalid service type: ${String(typeWithDot)} (expected e.g. "_http._tcp.")`;
|
|
231
|
+
}
|
|
232
|
+
normalizeTimeout(value) {
|
|
233
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
234
|
+
return DEFAULT_DISCOVER_TIMEOUT_MS;
|
|
235
|
+
return Math.min(Math.max(0, Math.trunc(value)), MAX_TIMER_MS);
|
|
236
|
+
}
|
|
237
|
+
toFullType(type, protocol) {
|
|
238
|
+
return `_${type}._${protocol}.`;
|
|
239
|
+
}
|
|
240
|
+
async safeStopService(svc) {
|
|
241
|
+
if (!svc)
|
|
242
|
+
return;
|
|
243
|
+
await new Promise((res, reject) => {
|
|
244
|
+
let settled = false;
|
|
245
|
+
const done = (error) => {
|
|
246
|
+
if (settled)
|
|
247
|
+
return;
|
|
248
|
+
settled = true;
|
|
249
|
+
if (timeout)
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
if (error)
|
|
252
|
+
reject(error);
|
|
253
|
+
else
|
|
254
|
+
res();
|
|
255
|
+
};
|
|
256
|
+
const timeout = setTimeout(() => done(new Error(`Timed out waiting for service stop after ${this.stopTimeoutMs}ms`)), this.stopTimeoutMs);
|
|
257
|
+
try {
|
|
258
|
+
svc.stop(done);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
done(e);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
safeStopBrowser(b) {
|
|
266
|
+
if (!b)
|
|
267
|
+
return;
|
|
268
|
+
try {
|
|
269
|
+
b.stop();
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.warn('[mDNS] browser.stop error:', err);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ----------------------------- API -----------------------------
|
|
276
|
+
/**
|
|
277
|
+
* Return the Electron main-process implementation marker.
|
|
278
|
+
*/
|
|
279
|
+
async getPluginPlatform() {
|
|
280
|
+
return { platform: 'electron' };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Publish (advertise) a single Bonjour/mDNS service via bonjour-service.
|
|
284
|
+
* @param options See MdnsBroadcastOptions for type/name/port/txt.
|
|
285
|
+
* @returns Result indicating whether publishing is active and the final name.
|
|
286
|
+
*/
|
|
287
|
+
async startBroadcast(options) {
|
|
288
|
+
return this.withBroadcastLock(async () => {
|
|
289
|
+
var _a, _b;
|
|
290
|
+
const safeOptions = (options !== null && options !== void 0 ? options : {});
|
|
291
|
+
const portError = (_b = (_a = this.validatePort(safeOptions.port)) !== null && _a !== void 0 ? _a : this.validateName(safeOptions.name)) !== null && _b !== void 0 ? _b : this.validateTxt(safeOptions.txt);
|
|
292
|
+
if (portError)
|
|
293
|
+
return { publishing: !!this.advertiser, name: '', error: true, errorMessage: portError };
|
|
294
|
+
if (this.destroyed)
|
|
295
|
+
return { publishing: false, name: '', error: true, errorMessage: 'mDNS instance is destroyed' };
|
|
296
|
+
if (this.networkError)
|
|
297
|
+
return { publishing: !!this.advertiser, name: '', error: true, errorMessage: this.networkError };
|
|
298
|
+
const parsed = this.parseType(safeOptions.type);
|
|
299
|
+
if (!parsed) {
|
|
300
|
+
return {
|
|
301
|
+
publishing: !!this.advertiser,
|
|
302
|
+
name: '',
|
|
303
|
+
error: true,
|
|
304
|
+
errorMessage: this.invalidTypeMessage(safeOptions.type),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const { type, protocol } = parsed;
|
|
308
|
+
const stopped = await this.stopBroadcastUnlocked();
|
|
309
|
+
if (stopped.error)
|
|
310
|
+
return Object.assign(Object.assign({}, stopped), { name: '' });
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
let settled = false;
|
|
313
|
+
let failing = false;
|
|
314
|
+
let timeout = null;
|
|
315
|
+
const safeResolve = (r) => {
|
|
316
|
+
if (!settled) {
|
|
317
|
+
settled = true;
|
|
318
|
+
if (timeout) {
|
|
319
|
+
clearTimeout(timeout);
|
|
320
|
+
timeout = null;
|
|
321
|
+
}
|
|
322
|
+
this.pendingPublishError = undefined;
|
|
323
|
+
resolve(r);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
try {
|
|
327
|
+
const svc = this.bonjour.publish({
|
|
328
|
+
name: typeof safeOptions.name === 'string' && safeOptions.name.trim()
|
|
329
|
+
? safeOptions.name.trim()
|
|
330
|
+
: 'DevIOArtsMDNS',
|
|
331
|
+
type,
|
|
332
|
+
protocol,
|
|
333
|
+
port: safeOptions.port,
|
|
334
|
+
txt: this.isRecord(safeOptions.txt) ? this.normalizeTxt(safeOptions.txt) : undefined,
|
|
335
|
+
});
|
|
336
|
+
const cleanupListeners = () => {
|
|
337
|
+
try {
|
|
338
|
+
svc.removeListener('up', onUp);
|
|
339
|
+
svc.removeListener('error', onError);
|
|
340
|
+
}
|
|
341
|
+
catch (_a) {
|
|
342
|
+
/* ignore */
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const onUp = () => {
|
|
346
|
+
if (failing || settled)
|
|
347
|
+
return;
|
|
348
|
+
cleanupListeners();
|
|
349
|
+
safeResolve({ publishing: true, name: svc.name || '', error: false, errorMessage: null });
|
|
350
|
+
};
|
|
351
|
+
const onError = async (err) => {
|
|
352
|
+
if (failing || settled)
|
|
353
|
+
return;
|
|
354
|
+
failing = true;
|
|
355
|
+
if (timeout)
|
|
356
|
+
clearTimeout(timeout);
|
|
357
|
+
timeout = null;
|
|
358
|
+
cleanupListeners();
|
|
359
|
+
const stopped = await this.stopBroadcastUnlocked();
|
|
360
|
+
safeResolve({
|
|
361
|
+
publishing: stopped.publishing,
|
|
362
|
+
name: '',
|
|
363
|
+
error: true,
|
|
364
|
+
errorMessage: [this.toErr(err), stopped.errorMessage].filter(Boolean).join('; '),
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const onTimeout = () => {
|
|
368
|
+
void onError(new Error(svc.activated === false
|
|
369
|
+
? 'Service name is already in use on the network'
|
|
370
|
+
: `Timed out waiting for service publish after ${this.publishTimeoutMs}ms`));
|
|
371
|
+
};
|
|
372
|
+
// shortcut: Bonjour 1.4 sends goodbye for conflicting probes; remove this hook when its probe teardown is fixed.
|
|
373
|
+
const serviceRecords = svc.records;
|
|
374
|
+
let announcementStarted = svc.published === true;
|
|
375
|
+
svc.records = () => {
|
|
376
|
+
if (svc.destroyed)
|
|
377
|
+
return [];
|
|
378
|
+
// Bonjour sets published only after the send callback, but records are already registered before it.
|
|
379
|
+
announcementStarted = true;
|
|
380
|
+
return serviceRecords.call(svc);
|
|
381
|
+
};
|
|
382
|
+
const stop = svc.stop.bind(svc);
|
|
383
|
+
svc.stop = (callback) => {
|
|
384
|
+
const announced = announcementStarted || svc.published === true;
|
|
385
|
+
const conflict = !callback && svc.activated && !announced;
|
|
386
|
+
const records = svc.records;
|
|
387
|
+
if (!announced)
|
|
388
|
+
svc.records = () => [];
|
|
389
|
+
try {
|
|
390
|
+
stop(callback);
|
|
391
|
+
svc.destroyed = true;
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
svc.records = records;
|
|
395
|
+
}
|
|
396
|
+
if (conflict)
|
|
397
|
+
void onError(new Error('Service name is already in use on the network'));
|
|
398
|
+
};
|
|
399
|
+
svc.once('up', onUp);
|
|
400
|
+
svc.once('error', onError);
|
|
401
|
+
timeout = setTimeout(onTimeout, this.publishTimeoutMs);
|
|
402
|
+
this.advertiser = svc;
|
|
403
|
+
this.pendingPublishError = (error) => {
|
|
404
|
+
void onError(error);
|
|
405
|
+
};
|
|
406
|
+
if (this.networkError)
|
|
407
|
+
this.pendingPublishError(new Error(this.networkError));
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
safeResolve({ publishing: false, name: '', error: true, errorMessage: this.toErr(e) });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Stop advertising the current service if any and clear internal state.
|
|
417
|
+
* @returns Result indicating whether the advertiser is active and error info.
|
|
418
|
+
*/
|
|
419
|
+
async stopBroadcast() {
|
|
420
|
+
return this.withBroadcastLock(() => this.stopBroadcastUnlocked());
|
|
421
|
+
}
|
|
422
|
+
async stopBroadcastUnlocked() {
|
|
423
|
+
try {
|
|
424
|
+
if (this.advertiser) {
|
|
425
|
+
await this.safeStopService(this.advertiser);
|
|
426
|
+
this.advertiser = undefined;
|
|
427
|
+
}
|
|
428
|
+
return { publishing: false, error: false, errorMessage: null };
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
return { publishing: !!this.advertiser, error: true, errorMessage: this.toErr(e) };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Discover services of the given type and optionally filter by instance name.
|
|
436
|
+
* Deduplicates by (name:port), collects IPv4/IPv6 addresses and TXT, and
|
|
437
|
+
* resolves with either an early-exit match or after a timeout.
|
|
438
|
+
* @param options See MdnsDiscoverOptions for type/name/timeout.
|
|
439
|
+
*/
|
|
440
|
+
async discover(options = {}) {
|
|
441
|
+
var _a;
|
|
442
|
+
const safeOptions = (options !== null && options !== void 0 ? options : {});
|
|
443
|
+
if (this.destroyed) {
|
|
444
|
+
return { error: true, errorMessage: 'mDNS instance is destroyed', servicesFound: 0, services: [] };
|
|
445
|
+
}
|
|
446
|
+
if (this.networkError)
|
|
447
|
+
return { error: true, errorMessage: this.networkError, servicesFound: 0, services: [] };
|
|
448
|
+
const parsed = this.parseType(safeOptions.type);
|
|
449
|
+
if (!parsed) {
|
|
450
|
+
return {
|
|
451
|
+
error: true,
|
|
452
|
+
errorMessage: this.invalidTypeMessage(safeOptions.type),
|
|
453
|
+
servicesFound: 0,
|
|
454
|
+
services: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const inputError = (_a = this.validateName(safeOptions.name)) !== null && _a !== void 0 ? _a : (safeOptions.timeout != null && (typeof safeOptions.timeout !== 'number' || !Number.isFinite(safeOptions.timeout))
|
|
458
|
+
? 'Invalid timeout'
|
|
459
|
+
: null);
|
|
460
|
+
if (inputError)
|
|
461
|
+
return { error: true, errorMessage: inputError, servicesFound: 0, services: [] };
|
|
462
|
+
for (const abort of [...this.activeDiscoveries])
|
|
463
|
+
abort('Discovery replaced by a new request');
|
|
464
|
+
const { type, protocol } = parsed;
|
|
465
|
+
const targetId = typeof safeOptions.name === 'string' && safeOptions.name.trim() ? safeOptions.name.trim() : null;
|
|
466
|
+
const timeoutMs = this.normalizeTimeout(safeOptions.timeout);
|
|
467
|
+
let browser = null;
|
|
468
|
+
const services = [];
|
|
469
|
+
let hadError = false;
|
|
470
|
+
let errMsg = null;
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
var _a;
|
|
473
|
+
let timer = null;
|
|
474
|
+
let settled = false;
|
|
475
|
+
const finish = (abortMessage) => {
|
|
476
|
+
var _a;
|
|
477
|
+
if (settled)
|
|
478
|
+
return;
|
|
479
|
+
settled = true;
|
|
480
|
+
this.activeDiscoveries.delete(finish);
|
|
481
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.removeListener('response', onResponse);
|
|
482
|
+
if (abortMessage) {
|
|
483
|
+
hadError = true;
|
|
484
|
+
errMsg = abortMessage;
|
|
485
|
+
}
|
|
486
|
+
if (browser) {
|
|
487
|
+
this.safeStopBrowser(browser);
|
|
488
|
+
browser = null;
|
|
489
|
+
}
|
|
490
|
+
if (timer) {
|
|
491
|
+
clearTimeout(timer);
|
|
492
|
+
timer = null;
|
|
493
|
+
}
|
|
494
|
+
resolve({
|
|
495
|
+
error: hadError,
|
|
496
|
+
errorMessage: errMsg,
|
|
497
|
+
services: hadError ? [] : services.filter((s) => s.hosts.length > 0),
|
|
498
|
+
servicesFound: hadError ? 0 : services.filter((s) => s.hosts.length > 0).length,
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
const up = (s) => {
|
|
502
|
+
if (settled)
|
|
503
|
+
return;
|
|
504
|
+
const item = this.serviceResult(s, this.toFullType(type, protocol));
|
|
505
|
+
if (!this.matchesTarget(item.name, targetId))
|
|
506
|
+
return;
|
|
507
|
+
const previous = services.find((x) => x.name === item.name && x.port === item.port);
|
|
508
|
+
if (previous)
|
|
509
|
+
Object.assign(previous, item, { hosts: this.orderHosts([...previous.hosts, ...item.hosts]) });
|
|
510
|
+
else
|
|
511
|
+
services.push(item);
|
|
512
|
+
if (targetId && item.hosts.length)
|
|
513
|
+
finish();
|
|
514
|
+
else if (!item.hosts.length && item.hostname) {
|
|
515
|
+
this.queryAddresses(item.hostname);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const onResponse = (packet) => {
|
|
519
|
+
var _a, _b, _c;
|
|
520
|
+
if (settled)
|
|
521
|
+
return;
|
|
522
|
+
const records = [...((_a = packet.answers) !== null && _a !== void 0 ? _a : []), ...((_b = packet.additionals) !== null && _b !== void 0 ? _b : [])];
|
|
523
|
+
for (const item of services) {
|
|
524
|
+
for (const record of records) {
|
|
525
|
+
if ((record.type === 'A' || record.type === 'AAAA') &&
|
|
526
|
+
record.ttl &&
|
|
527
|
+
typeof record.data === 'string' &&
|
|
528
|
+
record.name.toLowerCase() === ((_c = item.hostname) === null || _c === void 0 ? void 0 : _c.toLowerCase()) &&
|
|
529
|
+
!item.hosts.includes(record.data))
|
|
530
|
+
item.hosts = this.orderHosts([...item.hosts, record.data]);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (targetId && services.some((item) => item.hosts.length))
|
|
534
|
+
finish();
|
|
535
|
+
};
|
|
536
|
+
this.activeDiscoveries.add(finish);
|
|
537
|
+
try {
|
|
538
|
+
const bonjour = this.bonjour;
|
|
539
|
+
if (settled)
|
|
540
|
+
return;
|
|
541
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.on('response', onResponse);
|
|
542
|
+
browser = bonjour.find({ type, protocol }, up);
|
|
543
|
+
if (settled) {
|
|
544
|
+
this.safeStopBrowser(browser);
|
|
545
|
+
browser = null;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
browser.on('srv-update', up);
|
|
549
|
+
browser.on('txt-update', up);
|
|
550
|
+
// Record error and let the timeout conclude; results will still be returned.
|
|
551
|
+
const onBrowserError = (err) => {
|
|
552
|
+
hadError = true;
|
|
553
|
+
errMsg = this.toErr(err);
|
|
554
|
+
};
|
|
555
|
+
browser.on('error', onBrowserError);
|
|
556
|
+
timer = setTimeout(() => finish(), timeoutMs);
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
hadError = true;
|
|
560
|
+
errMsg = this.toErr(err);
|
|
561
|
+
finish();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
normalizeTxt(txt) {
|
|
566
|
+
return Object.fromEntries(Object.entries(txt).map(([key, value]) => [key, Buffer.isBuffer(value) ? value.toString('utf8') : String(value)]));
|
|
567
|
+
}
|
|
568
|
+
queryAddresses(hostname) {
|
|
569
|
+
var _a, _b, _c, _d;
|
|
570
|
+
try {
|
|
571
|
+
(_b = (_a = this.transport) === null || _a === void 0 ? void 0 : _a.query) === null || _b === void 0 ? void 0 : _b.call(_a, hostname, 'A');
|
|
572
|
+
(_d = (_c = this.transport) === null || _c === void 0 ? void 0 : _c.query) === null || _d === void 0 ? void 0 : _d.call(_c, hostname, 'AAAA');
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
this.onNetworkError(new Error(this.toErr(err)));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
orderHosts(hosts) {
|
|
579
|
+
return [...new Set(hosts)].sort((a, b) => Number(node_net.isIPv4(b)) - Number(node_net.isIPv4(a)));
|
|
580
|
+
}
|
|
581
|
+
serviceResult(s, type) {
|
|
582
|
+
var _a, _b;
|
|
583
|
+
const suffix = `.${type}local`;
|
|
584
|
+
const name = ((_a = s.fqdn) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith(suffix.toLowerCase())) ? s.fqdn.slice(0, -suffix.length) : s.name || '';
|
|
585
|
+
return Object.assign(Object.assign({ name,
|
|
586
|
+
type, domain: 'local.', port: (_b = s.port) !== null && _b !== void 0 ? _b : 0, hosts: this.orderHosts(Array.isArray(s.addresses) ? s.addresses : []) }, (s.host ? { hostname: s.host } : {})), (s.txt && Object.keys(s.txt).length ? { txt: this.normalizeTxt(s.txt) } : {}));
|
|
587
|
+
}
|
|
588
|
+
notify(event, data) {
|
|
589
|
+
var _a;
|
|
590
|
+
this.emit(event, data);
|
|
591
|
+
for (const win of (_a = electron.BrowserWindow === null || electron.BrowserWindow === void 0 ? void 0 : electron.BrowserWindow.getAllWindows()) !== null && _a !== void 0 ? _a : []) {
|
|
592
|
+
if (!win.isDestroyed() && !win.webContents.isDestroyed() && this.trustedUrl(win.webContents.getURL())) {
|
|
593
|
+
win.webContents.send(`event-mDNS-${event}`, data);
|
|
594
|
+
win.webContents.send(`mdns:event:${event}`, data);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** Thanks to Idrimi for the original continuous discovery feature (PR #7). */
|
|
599
|
+
async watchServices(options) {
|
|
600
|
+
var _a;
|
|
601
|
+
const failed = (message) => ({ error: true, errorMessage: message, watchersStarted: 0 });
|
|
602
|
+
if (this.destroyed)
|
|
603
|
+
return failed('mDNS instance is destroyed');
|
|
604
|
+
if (this.networkError)
|
|
605
|
+
return failed(this.networkError);
|
|
606
|
+
if (!Array.isArray(options === null || options === void 0 ? void 0 : options.types) || !options.types.length)
|
|
607
|
+
return failed("Missing or empty 'types' array");
|
|
608
|
+
const types = new Map();
|
|
609
|
+
for (const raw of options.types) {
|
|
610
|
+
const parsed = typeof raw === 'string' && raw.trim() ? this.parseType(raw) : null;
|
|
611
|
+
if (!parsed)
|
|
612
|
+
return failed(this.invalidTypeMessage(raw));
|
|
613
|
+
types.set(this.toFullType(parsed.type, parsed.protocol), parsed);
|
|
614
|
+
}
|
|
615
|
+
const cleanup = this.stopWatch();
|
|
616
|
+
if (cleanup)
|
|
617
|
+
return failed(cleanup);
|
|
618
|
+
const bonjour = this.bonjour;
|
|
619
|
+
if (this.networkError)
|
|
620
|
+
return failed(this.networkError);
|
|
621
|
+
const watch = { types: [...types.keys()], browsers: [], services: new Map() };
|
|
622
|
+
this.watch = watch;
|
|
623
|
+
const addBrowser = (type, parsed) => {
|
|
624
|
+
let active = true;
|
|
625
|
+
const up = (s) => {
|
|
626
|
+
var _a;
|
|
627
|
+
if (this.watch !== watch || !active)
|
|
628
|
+
return;
|
|
629
|
+
const service = this.serviceResult(s, type);
|
|
630
|
+
const key = `${type}|${service.name}`;
|
|
631
|
+
const previous = watch.services.get(key);
|
|
632
|
+
if (!service.hosts.length && (previous === null || previous === void 0 ? void 0 : previous.service.hostname) === service.hostname)
|
|
633
|
+
service.hosts = [...((_a = previous === null || previous === void 0 ? void 0 : previous.service.hosts) !== null && _a !== void 0 ? _a : [])];
|
|
634
|
+
const isNew = !(previous === null || previous === void 0 ? void 0 : previous.announced) && service.hosts.length > 0;
|
|
635
|
+
watch.services.set(key, {
|
|
636
|
+
service,
|
|
637
|
+
fqdn: s.fqdn || `${service.name}.${type}local`,
|
|
638
|
+
expires: s.ttl ? Date.now() + s.ttl * 1000 : Infinity,
|
|
639
|
+
announced: !!(previous === null || previous === void 0 ? void 0 : previous.announced) || isNew,
|
|
640
|
+
});
|
|
641
|
+
if (!service.hosts.length && service.hostname)
|
|
642
|
+
this.queryAddresses(service.hostname);
|
|
643
|
+
if (isNew)
|
|
644
|
+
this.notify('serviceFound', { service: structuredClone(service) });
|
|
645
|
+
};
|
|
646
|
+
const browser = bonjour.find(parsed, up);
|
|
647
|
+
browser.on('down', (s) => {
|
|
648
|
+
if (this.watch !== watch || !active)
|
|
649
|
+
return;
|
|
650
|
+
const key = `${type}|${this.serviceResult(s, type).name}`;
|
|
651
|
+
const previous = watch.services.get(key);
|
|
652
|
+
watch.services.delete(key);
|
|
653
|
+
if (previous === null || previous === void 0 ? void 0 : previous.announced)
|
|
654
|
+
this.notify('serviceLost', { service: structuredClone(previous.service) });
|
|
655
|
+
});
|
|
656
|
+
browser.on('srv-update', up);
|
|
657
|
+
browser.on('txt-update', up);
|
|
658
|
+
this.deactivateBrowser.set(browser, () => {
|
|
659
|
+
active = false;
|
|
660
|
+
});
|
|
661
|
+
if (this.watch !== watch) {
|
|
662
|
+
this.stopWatchBrowser(browser);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
watch.browsers.push(browser);
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
for (const [type, parsed] of types)
|
|
669
|
+
addBrowser(type, parsed);
|
|
670
|
+
if (this.watch !== watch)
|
|
671
|
+
return failed('Watch stopped during startup');
|
|
672
|
+
watch.onResponse = (packet) => {
|
|
673
|
+
var _a, _b;
|
|
674
|
+
if (this.watch !== watch)
|
|
675
|
+
return;
|
|
676
|
+
const records = [...((_a = packet.answers) !== null && _a !== void 0 ? _a : []), ...((_b = packet.additionals) !== null && _b !== void 0 ? _b : [])];
|
|
677
|
+
for (const item of watch.services.values()) {
|
|
678
|
+
const ptr = records.find((r) => r.type === 'PTR' && String(r.data).toLowerCase() === item.fqdn.toLowerCase());
|
|
679
|
+
if (ptr)
|
|
680
|
+
item.expires = Date.now() + ptr.ttl * 1000;
|
|
681
|
+
const addresses = records.filter((r) => { var _a; return (r.type === 'A' || r.type === 'AAAA') && r.name.toLowerCase() === ((_a = item.service.hostname) === null || _a === void 0 ? void 0 : _a.toLowerCase()); });
|
|
682
|
+
for (const address of addresses) {
|
|
683
|
+
if (typeof address.data !== 'string')
|
|
684
|
+
continue;
|
|
685
|
+
const hosts = item.service.hosts.filter((host) => host !== address.data);
|
|
686
|
+
item.service.hosts = this.orderHosts(address.ttl ? [...hosts, address.data] : hosts);
|
|
687
|
+
}
|
|
688
|
+
if (!item.announced && item.service.hosts.length) {
|
|
689
|
+
item.announced = true;
|
|
690
|
+
this.notify('serviceFound', { service: structuredClone(item.service) });
|
|
691
|
+
if (this.watch !== watch)
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.on('response', watch.onResponse);
|
|
697
|
+
let ticks = 0;
|
|
698
|
+
watch.timer = setInterval(() => {
|
|
699
|
+
var _a;
|
|
700
|
+
if (this.watch !== watch)
|
|
701
|
+
return;
|
|
702
|
+
try {
|
|
703
|
+
const expired = [...watch.services.entries()].filter(([, item]) => item.expires <= Date.now());
|
|
704
|
+
for (const [key, item] of expired) {
|
|
705
|
+
watch.services.delete(key);
|
|
706
|
+
if (item.announced)
|
|
707
|
+
this.notify('serviceLost', { service: structuredClone(item.service) });
|
|
708
|
+
if (this.watch !== watch)
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// A fresh Browser forgets expired entries retained by bonjour-service's internal cache.
|
|
712
|
+
if (expired.length) {
|
|
713
|
+
for (const browser of watch.browsers.splice(0))
|
|
714
|
+
this.stopWatchBrowser(browser);
|
|
715
|
+
for (const [type, parsed] of types)
|
|
716
|
+
addBrowser(type, parsed);
|
|
717
|
+
}
|
|
718
|
+
else if (++ticks % 30 === 0) {
|
|
719
|
+
for (const browser of watch.browsers)
|
|
720
|
+
(_a = browser.update) === null || _a === void 0 ? void 0 : _a.call(browser);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
this.onNetworkError(err instanceof Error ? err : new Error(this.toErr(err)));
|
|
725
|
+
}
|
|
726
|
+
}, 1000);
|
|
727
|
+
watch.timer.unref();
|
|
728
|
+
return { error: false, errorMessage: null, watchersStarted: types.size };
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
this.stopWatch();
|
|
732
|
+
return failed(this.toErr(err));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
stopWatchBrowser(browser) {
|
|
736
|
+
var _a;
|
|
737
|
+
(_a = this.deactivateBrowser.get(browser)) === null || _a === void 0 ? void 0 : _a();
|
|
738
|
+
this.deactivateBrowser.delete(browser);
|
|
739
|
+
try {
|
|
740
|
+
browser.stop();
|
|
741
|
+
this.stoppedBrowsers.delete(browser);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
this.stoppedBrowsers.add(browser);
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
stopWatch() {
|
|
749
|
+
var _a, _b;
|
|
750
|
+
const watch = this.watch;
|
|
751
|
+
this.watch = undefined;
|
|
752
|
+
if (watch === null || watch === void 0 ? void 0 : watch.timer)
|
|
753
|
+
clearInterval(watch.timer);
|
|
754
|
+
if (watch === null || watch === void 0 ? void 0 : watch.onResponse)
|
|
755
|
+
(_a = this.transport) === null || _a === void 0 ? void 0 : _a.removeListener('response', watch.onResponse);
|
|
756
|
+
const errors = [];
|
|
757
|
+
for (const browser of new Set([...((_b = watch === null || watch === void 0 ? void 0 : watch.browsers) !== null && _b !== void 0 ? _b : []), ...this.stoppedBrowsers])) {
|
|
758
|
+
try {
|
|
759
|
+
this.stopWatchBrowser(browser);
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
errors.push(this.toErr(err));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return errors.length ? errors.join('; ') : null;
|
|
766
|
+
}
|
|
767
|
+
async stopWatching() {
|
|
768
|
+
const errorMessage = this.stopWatch();
|
|
769
|
+
return { watching: false, error: !!errorMessage, errorMessage };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
exports.mDNS = mDNS;
|
|
774
|
+
//# sourceMappingURL=plugin.cjs.js.map
|