import { Browser, Service, ServiceType } from "@astronautlabs/mdns"; import { Logger } from "@foxxmd/logging"; import AvahiBrowser from 'avahi-browse'; import { MdnsDeviceInfo } from "../common/infrastructure/Atomic.js"; import { MaybeLogger } from '../common/MaybeLogger.js'; import { sleep } from "../utils.js"; import { debounce, DebouncedFunction } from "./debounce.js"; export interface AvahiService { service_name: string target: { service_type: string domain: string host: string hostname: string port: string } } export interface DiscoveryOptions { sanity?: boolean onDiscover?: (service: MdnsDeviceInfo) => void onDnsError?: (err: Error) => void duration?: number, logger?: Logger } export const discoveryAvahi = async (service: string, options?: DiscoveryOptions) => { const { logger, duration = 10000, sanity = false, onDiscover, onDnsError } = options; const maybeLogger = new MaybeLogger(logger, 'Avahi mDNS'); maybeLogger.debug(`Starting mDNS discovery with Avahi => Listening for ${(duration / 1000).toFixed(2)}s`); let anyDiscovered = false; const services = new Map(); const triggerDiscovery = () => { for(const [k,v] of services.entries()) { maybeLogger.debug(`Discovered device "${v.name}" with ${v.addresses.length} interfaces`); onDiscover(v); services.delete(k); } } let debouncedFunc: DebouncedFunction; try { const browser = new AvahiBrowser(service); browser.on(AvahiBrowser.EVENT_SERVICE_UP, async (service: AvahiService) => { anyDiscovered = true; if(onDiscover !== undefined) { let foundService = services.get(service.service_name); if(foundService === undefined) { foundService = { name: service.service_name, addresses: [service.target.host], type: service.target.service_type, } } else { foundService.addresses.push(service.target.host); } services.set(foundService.name, foundService); if(debouncedFunc === undefined) { debouncedFunc = debounce(() => triggerDiscovery(), 1000); } else { await debouncedFunc(); } } }); browser.on(AvahiBrowser.EVENT_DNSSD_ERROR, (err) => { const e = new Error('Error occurred while using avahi-browse', {cause: err}); if (onDnsError) { onDnsError(e) } else { maybeLogger.error(e); } }); browser.start(); if (sanity) { await sleep(1500); if (!anyDiscovered) { maybeLogger.debug('Did not find any mdns services after 1.5s! Do you have port 5353 open?'); } await sleep(duration - 1500); } else { await sleep(duration); } maybeLogger.debug('Stopped discovery'); } catch (e) { maybeLogger.warn(new Error('mDNS device discovery with avahi-browse failed', {cause: e})); } } export const discoveryNative = async (service: string, options?: DiscoveryOptions) => { const { logger, duration = 10000, sanity = false, onDiscover, onDnsError } = options; const maybeLogger = new MaybeLogger(logger, 'mDNS'); maybeLogger.debug(`Starting mDNS discovery => Listening for ${(duration / 1000).toFixed(2)}s`); if (sanity) { const services: ServiceType[] = []; const testBrowser = new Browser(ServiceType.all()) .on('serviceUp', (service: ServiceType) => { services.push(service) }) .start(); testBrowser.on('error', (err) => { maybeLogger.error(new Error('Error occurred during mDNS service discovery', {cause: err})); }); maybeLogger.debug('Waiting 1s to gather advertised mdns services...'); await sleep(1000); testBrowser.stop(); if (services.length === 0) { maybeLogger.debug('Did not find any mdns services! Do you have port 5353 open?'); } else { maybeLogger.debug(`Found services: ${services.map(x => `${x.name}-${x.protocol}`).join(' ,')}`); } } const browser = new Browser(service, {resolve: true}) .on('serviceUp', async (service) => { maybeLogger.debug(`Discovered device "${service.name}" with ${service.addresses.length} interfaces`); if (onDiscover) { onDiscover({name: service.name, addresses: service.addresses, type: service.service_type}); } }) browser.on('error', (err) => { const e = new Error('Error occurred during mDNS discovery', {cause: err}); if (onDnsError) { onDnsError(e) } else { maybeLogger.error(e); } }); browser.start(); await sleep(duration); maybeLogger.debug('Stopped discovery'); browser.stop(); }