use mdns_sd::ServiceDaemon;
use std::collections::HashMap;
use std::net::Ipv4Addr;
const SERVICE_TYPE: &str = "_pitchfork._tcp.local.";
pub struct MdnsPublisher {
daemon: ServiceDaemon,
registrations: HashMap<String, String>,
lan_ip: Ipv4Addr,
shutdown: bool,
}
impl MdnsPublisher {
pub fn new(lan_ip: Ipv4Addr) -> Option<Self> {
let daemon = ServiceDaemon::new().ok()?;
log::info!("mDNS publisher started with LAN IP {lan_ip}");
Some(Self {
daemon,
registrations: HashMap::new(),
lan_ip,
shutdown: false,
})
}
pub fn publish(&mut self, hostname: &str, port: u16) {
if self.shutdown {
return;
}
if self.registrations.contains_key(hostname) {
return;
}
let instance_name = hostname.strip_suffix(".local").unwrap_or(hostname);
let host_name = format!("{hostname}.");
let service = match mdns_sd::ServiceInfo::new(
SERVICE_TYPE,
instance_name,
&host_name,
self.lan_ip.to_string(),
port,
&[] as &[(&str, &str)],
) {
Ok(s) => s,
Err(e) => {
log::warn!("mDNS: failed to build ServiceInfo for {hostname}: {e}");
return;
}
};
let fullname = service.get_fullname().to_string();
match self.daemon.register(service) {
Ok(_) => {
log::info!("mDNS: published {hostname} → {lan}", lan = self.lan_ip);
self.registrations.insert(hostname.to_string(), fullname);
}
Err(e) => {
log::warn!("mDNS: failed to register {hostname}: {e}");
}
}
}
pub fn unpublish(&mut self, hostname: &str) {
if self.shutdown {
return;
}
if let Some(fullname) = self.registrations.remove(hostname) {
if let Err(e) = self.daemon.unregister(&fullname) {
log::warn!("mDNS: failed to unregister {hostname}: {e}");
} else {
log::info!("mDNS: unpublished {hostname}");
}
}
}
pub fn republish_all(&mut self, new_ip: Ipv4Addr, port: u16) {
if self.shutdown || new_ip == self.lan_ip {
return;
}
let hostnames: Vec<String> = self.registrations.keys().cloned().collect();
for hostname in &hostnames {
if let Some(fullname) = self.registrations.remove(hostname) {
let _ = self.daemon.unregister(&fullname);
}
}
let old_ip = self.lan_ip;
self.lan_ip = new_ip;
log::info!(
"mDNS: LAN IP changed {old_ip} → {new_ip}, re-publishing {} records",
hostnames.len()
);
for hostname in &hostnames {
self.publish(hostname, port);
}
}
#[allow(dead_code)]
pub fn lan_ip(&self) -> Ipv4Addr {
self.lan_ip
}
pub fn registered_hostnames(&self) -> Vec<String> {
self.registrations.keys().cloned().collect()
}
pub fn is_published(&self, hostname: &str) -> bool {
self.registrations.contains_key(hostname)
}
pub fn shutdown(&mut self) {
if self.shutdown {
return;
}
self.shutdown = true;
self.registrations.clear();
if let Err(e) = self.daemon.shutdown() {
log::warn!("mDNS: shutdown error: {e}");
} else {
log::info!("mDNS: publisher shut down");
}
}
}