homebridge-smartsystem
Version:
SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
267 lines • 10.9 kB
JavaScript
;
// 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