use std::net::IpAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use mdns_sd::{ServiceDaemon, ServiceEvent};
use crate::SERVICE_TYPE;
use crate::error::DiscoveryError;
use crate::txt::TxtRecord;
#[derive(Debug, Clone)]
pub struct DiscoveredServer {
pub instance_name: String,
pub hostname: String,
pub port: u16,
pub addresses: Vec<IpAddr>,
pub txt: TxtRecord,
pub last_seen: Instant,
}
#[derive(Debug, Clone)]
pub enum DiscoveryEvent {
ServerAnnounced(DiscoveredServer),
ServerWithdrawn {
instance_name: String,
},
}
pub struct Browser {
daemon: ServiceDaemon,
shutdown: Arc<AtomicBool>,
listener: Option<JoinHandle<()>>,
}
impl Browser {
pub fn start<F>(mut on_event: F) -> Result<Self, DiscoveryError>
where
F: FnMut(DiscoveryEvent) + Send + 'static,
{
let daemon = ServiceDaemon::new()?;
let receiver = daemon.browse(SERVICE_TYPE)?;
let shutdown = Arc::new(AtomicBool::new(false));
let listener_shutdown = shutdown.clone();
let listener = thread::Builder::new()
.name("rtl_tcp-discovery-browser".into())
.spawn(move || {
while !listener_shutdown.load(Ordering::Relaxed) {
match receiver.recv_timeout(Duration::from_millis(100)) {
Ok(event) => {
if let Some(e) = translate(event) {
on_event(e);
}
}
Err(_) if receiver.is_disconnected() => {
tracing::debug!("mDNS browser channel closed, exiting listener thread");
return;
}
Err(_) => {}
}
}
})
.map_err(DiscoveryError::Io)?;
Ok(Self {
daemon,
shutdown,
listener: Some(listener),
})
}
pub fn stop(mut self) {
self.initiate_shutdown();
if let Some(h) = self.listener.take() {
let _ = h.join();
}
}
fn initiate_shutdown(&self) {
self.shutdown.store(true, Ordering::SeqCst);
let _ = self.daemon.stop_browse(SERVICE_TYPE);
let _ = self.daemon.shutdown();
}
}
impl Drop for Browser {
fn drop(&mut self) {
self.initiate_shutdown();
if let Some(h) = self.listener.take() {
let _ = h.join();
}
}
}
fn translate(event: ServiceEvent) -> Option<DiscoveryEvent> {
match event {
ServiceEvent::ServiceResolved(resolved) => {
let mut addresses: Vec<IpAddr> = resolved
.get_addresses()
.iter()
.map(mdns_sd::ScopedIp::to_ip_addr)
.collect();
addresses.sort_by_key(|ip| match ip {
IpAddr::V4(_) => 0,
IpAddr::V6(_) => 1,
});
let txt = TxtRecord::from_properties(
resolved
.get_properties()
.iter()
.map(|p| (p.key().to_string(), p.val_str().to_string())),
);
Some(DiscoveryEvent::ServerAnnounced(DiscoveredServer {
instance_name: resolved.get_fullname().to_string(),
hostname: resolved.get_hostname().to_string(),
port: resolved.get_port(),
addresses,
txt,
last_seen: Instant::now(),
}))
}
ServiceEvent::ServiceRemoved(_, name) => Some(DiscoveryEvent::ServerWithdrawn {
instance_name: name,
}),
ServiceEvent::SearchStarted(_)
| ServiceEvent::SearchStopped(_)
| ServiceEvent::ServiceFound(_, _) => None,
_ => None,
}
}