#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] use std::{ collections::HashMap, net::SocketAddr, time::{Duration, Instant}, }; use eyre::bail; pub use mdns_sd::DaemonEvent; use mdns_sd::{Receiver, ResolvedService, ServiceDaemon, ServiceEvent, ServiceInfo}; pub const LANSPREAD_SERVICE_TYPE: &str = "_lanspread._udp.local."; pub type MdnsMonitor = Receiver; pub struct MdnsAdvertiser { daemon: ServiceDaemon, service_info: ServiceInfo, pub monitor: Receiver, } impl MdnsAdvertiser { pub fn new( service_type: &str, instance_name: &str, address: SocketAddr, properties: Option>, ) -> eyre::Result { let host_name = format!("{}.local.", address.ip()); let daemon = ServiceDaemon::new()?; let service_info = ServiceInfo::new( service_type, instance_name, &host_name, address.ip(), address.port(), properties, )?; let monitor = daemon.monitor()?; // Register the service daemon.register(service_info.clone())?; Ok(Self { daemon, service_info, monitor, }) } } impl Drop for MdnsAdvertiser { fn drop(&mut self) { let _ = self.daemon.unregister(self.service_info.get_fullname()); let _ = self.daemon.shutdown(); } } pub struct MdnsBrowser { daemon: ServiceDaemon, receiver: Receiver, service_type: String, } #[derive(Debug, Clone)] pub struct MdnsService { pub addr: SocketAddr, pub fullname: String, pub hostname: String, pub properties: HashMap, } #[derive(Debug, Clone)] pub enum MdnsServicePoll { Service(MdnsService), Timeout, Closed, } impl MdnsBrowser { pub fn new(service_type: &str) -> eyre::Result { let daemon = ServiceDaemon::new()?; let receiver = daemon.browse(service_type)?; Ok(Self { daemon, receiver, service_type: service_type.to_string(), }) } pub fn next_service( &self, ignore_addr: Option, ) -> eyre::Result> { loop { match self.receiver.recv() { Ok(event) => { if let Some(service) = self.service_from_event(event, ignore_addr) { return Ok(Some(service)); } } Err(err) => { log::error!("mDNS browse channel closed: {err}"); return Ok(None); } } } } pub fn next_service_timeout( &self, ignore_addr: Option, timeout: Duration, ) -> eyre::Result { let deadline = Instant::now() + timeout; loop { let remaining = deadline.saturating_duration_since(Instant::now()); if remaining.is_zero() { return Ok(MdnsServicePoll::Timeout); } match self.receiver.recv_timeout(remaining) { Ok(event) => { if let Some(service) = self.service_from_event(event, ignore_addr) { return Ok(MdnsServicePoll::Service(service)); } } Err(err) if self.receiver.is_disconnected() => { log::error!("mDNS browse channel closed: {err}"); return Ok(MdnsServicePoll::Closed); } Err(err) => { log::trace!("mDNS browse timeout: {err}"); return Ok(MdnsServicePoll::Timeout); } } } } pub fn next_address( &self, ignore_addr: Option, ) -> eyre::Result> { Ok(self.next_service(ignore_addr)?.map(|service| service.addr)) } fn service_from_event( &self, event: ServiceEvent, ignore_addr: Option, ) -> Option { match event { ServiceEvent::ServiceResolved(info) => self.service_from_resolved(&info, ignore_addr), other_event => { log::trace!("mdns unrelated event: {other_event:?}"); None } } } fn service_from_resolved( &self, info: &ResolvedService, ignore_addr: Option, ) -> Option { log::trace!("mdns ServiceResolved event: {info:?}"); if info.ty_domain != self.service_type { log::trace!( "Got mDNS with uninteresting service type: {} (expected: {})", info.ty_domain, self.service_type, ); return None; } let mut ignored_match = false; for address in info.get_addresses() { let addr = SocketAddr::new(address.to_ip_addr(), info.get_port()); if ignore_addr.is_some_and(|ignore| ignore == addr) { ignored_match = true; log::trace!("Ignoring mDNS advertisement for local server at {addr}"); continue; } log::info!("Found server at {addr}"); let properties = info.get_properties().clone().into_property_map_str(); return Some(MdnsService { addr, fullname: info.get_fullname().to_string(), hostname: info.get_hostname().to_string(), properties, }); } if ignored_match { log::trace!( "Only saw ignored mDNS advertisements (probably ourselves) for {:?}", info.get_fullname() ); return None; } log::error!("No address found in mDNS response: {info:?}"); None } } impl Drop for MdnsBrowser { fn drop(&mut self) { let _ = self.daemon.shutdown(); } } pub fn discover_service( service_type: &str, ignore_addr: Option, ) -> eyre::Result { // Currently unused; kept for potential one-off discovery callers that just need a single address. let browser = MdnsBrowser::new(service_type)?; match browser.next_address(ignore_addr)? { Some(addr) => Ok(addr), None => bail!("No server found."), } }