UNPKG

homebridge-smartsystem

Version:

SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)

267 lines 10.9 kB
"use strict"; // mDNS Service Manager // Purpose: Handle mDNS service discovery and hostname publishing // Similar to ESP32's MDNS.begin() functionality // // Johan Coppieters, Nov 2025 Object.defineProperty(exports, "__esModule", { value: true }); exports.MDNSService = void 0; const Bonjour = require("bonjour-service"); const mdns = require("multicast-dns"); const os = require("os"); const logger_1 = require("../duotecno/logger"); class MDNSService { constructor(config) { this.registeredName = null; // Actual name registered (may have suffix) this.registrationError = null; // Last error during registration this.registrationStatus = 'pending'; this.config = config; } /** * Initialize mDNS service (call this before registering) */ init() { try { this.bonjour = new Bonjour.Bonjour(); (0, logger_1.log)("mDNS", "Service initialized"); } catch (e) { (0, logger_1.err)("mDNS", `Failed to initialize: ${e.message}`); throw e; } } /** * Register both HTTP service and hostname (like ESP32 MDNS.begin()) * Tries alternative names with suffixes if the name is already in use */ register() { if (!this.bonjour) { this.registrationError = "Service not initialized"; this.registrationStatus = 'failed'; (0, logger_1.err)("mDNS", "Service not initialized, call init() first"); return; } const { name, port, version } = this.config; const ports = Array.isArray(port) ? port : [port]; const primaryPort = ports[0]; // Set up error handler to prevent crash on "already in use" errors // Note: The error message will still be logged by Node.js, but the app won't crash // The retry logic with suffixes (.2, .3, etc.) will continue to work let handlerActive = true; const globalErrorHandler = (error) => { if (handlerActive && error.message.includes('already in use')) { // Suppress crash - retry logic will handle it return; } }; process.prependListener('uncaughtException', globalErrorHandler); setTimeout(() => { handlerActive = false; process.removeListener('uncaughtException', globalErrorHandler); }, 5000); // Try to register with the original name, then try with suffixes .2, .3, .4, .5, .6 this.tryRegisterWithSuffix(name, primaryPort, ports, version, 0); } /** * Recursively try to register with different suffixes */ tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt) { if (attempt > 6) { // Exhausted all attempts this.registrationError = `All names from "${baseName}" to "${baseName}.7" are already in use on the network`; this.registrationStatus = 'failed'; (0, logger_1.err)("mDNS", this.registrationError); (0, logger_1.err)("mDNS", "Please change the mdnsName in settings or stop other conflicting services"); return; } const tryName = attempt === 0 ? baseName : `${baseName}.${attempt + 1}`; let service; let conflictDetected = false; let successTimeout = null; // Create a wrapper to handle conflicts const handleConflict = () => { if (conflictDetected) return; conflictDetected = true; // Clear the success timeout if (successTimeout) { clearTimeout(successTimeout); successTimeout = null; } // Unpublish the conflicting service if (service) { try { service.stop(); } catch (e) { // Ignore errors during stop } } (0, logger_1.err)("mDNS", `Name "${tryName}" is already in use, trying next suffix...`); // Try next suffix setImmediate(() => { this.tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt + 1); }); }; try { service = this.bonjour.publish({ name: tryName, type: 'http', port: primaryPort, txt: { version: version || '1.0.0', ports: ports.join(','), path: '/' } }); // Set up error handler on the service if (service) { service.on('error', (error) => { if (error.message.includes('already in use')) { handleConflict(); } else if (!conflictDetected) { this.registrationError = error.message; this.registrationStatus = 'failed'; (0, logger_1.err)("mDNS", `Service error: ${error.message}`); } }); } // Wait longer before declaring success - conflicts come from network successTimeout = setTimeout(() => { if (!conflictDetected) { // Success! this.registeredName = tryName; this.registrationStatus = 'success'; this.registrationError = null; if (attempt > 0) { (0, logger_1.log)("mDNS", `⚠️ Original name "${baseName}" was taken, registered as "${tryName}.local" instead`); } else { (0, logger_1.log)("mDNS", `✓ HTTP service registered: ${tryName}.local on port(s) ${ports.join(', ')}`); } // Publish the hostname A record (like ESP32 MDNS.begin()) this.publishHostname(tryName); } }, 2000); // Wait 2 seconds for network conflict responses } catch (publishError) { if (publishError.message && publishError.message.includes('already in use')) { // This name is taken, try the next one (0, logger_1.err)("mDNS", `Name "${tryName}" is taken (sync error), trying next suffix...`); setImmediate(() => { this.tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt + 1); }); } else { // Different error, stop trying this.registrationError = publishError.message; this.registrationStatus = 'failed'; (0, logger_1.err)("mDNS", `Service publish error: ${publishError.message}`); } } } /** * Publish hostname as A/AAAA records (ESP32-style hostname publishing) */ publishHostname(hostname) { try { // Create multicast DNS instance this.mdns = mdns(); // Get local IP addresses const addresses = this.getLocalIPAddresses(); if (addresses.length === 0) { (0, logger_1.err)("mDNS", "No network interfaces found, cannot publish hostname"); return; } (0, logger_1.log)("mDNS", `Publishing hostname ${hostname}.local with IPs: ${addresses.join(', ')}`); // Respond to mDNS queries for our hostname (like ESP32 MDNS.begin()) this.mdns.on('query', (query) => { // Check if someone is asking for our hostname const hostnameQuery = query.questions.find(q => q.name === `${hostname}.local` && (q.type === 'A' || q.type === 'AAAA' || q.type === 'ANY')); if (hostnameQuery) { // Respond with A records for all our IP addresses const answers = addresses.map(ip => { const isIPv6 = ip.includes(':'); return { name: `${hostname}.local`, type: isIPv6 ? 'AAAA' : 'A', ttl: 120, data: ip }; }); this.mdns.respond(answers, () => { (0, logger_1.debug)("mDNS", `Responded to query for ${hostname}.local`); }); } }); // Send announcement (like ESP32 does on startup) this.announceHostname(hostname, addresses); (0, logger_1.log)("mDNS", `Hostname responder started for ${hostname}.local`); } catch (e) { (0, logger_1.err)("mDNS", `Failed to publish hostname: ${e.message}`); } } /** * Send unsolicited announcement (like ESP32 mDNS does on startup) */ announceHostname(hostname, addresses) { const answers = addresses.map(ip => { const isIPv6 = ip.includes(':'); return { name: `${hostname}.local`, type: isIPv6 ? 'AAAA' : 'A', ttl: 120, data: ip }; }); this.mdns.respond(answers, () => { (0, logger_1.log)("mDNS", `Announced ${hostname}.local on the network`); }); } /** * Get local IPv4 addresses (excluding loopback and virtual interfaces) */ getLocalIPAddresses() { const addresses = []; const interfaces = os.networkInterfaces(); for (const name in interfaces) { const iface = interfaces[name]; if (!iface) continue; for (const addr of iface) { // Skip internal (loopback) and non-IPv4 addresses if (addr.internal || addr.family !== 'IPv4') continue; // Skip docker and virtual interfaces if (name.startsWith('docker') || name.startsWith('veth')) continue; addresses.push(addr.address); } } return addresses; } /** * Stop and cleanup mDNS services */ destroy() { try { if (this.mdns) { this.mdns.destroy(); this.mdns = null; (0, logger_1.log)("mDNS", "Hostname responder stopped"); } if (this.bonjour) { this.bonjour.destroy(); this.bonjour = null; (0, logger_1.log)("mDNS", "Service discovery stopped"); } } catch (e) { (0, logger_1.err)("mDNS", `Error during cleanup: ${e.message}`); } } } exports.MDNSService = MDNSService; //# sourceMappingURL=mDNS.js.map