use std::collections::HashSet;
use std::net::IpAddr;
use std::num::NonZeroU32;
use std::str::FromStr;
use futures_util::stream::StreamExt;
use log::{trace, warn};
use tokio::sync::mpsc::unbounded_channel;
use tokio::task::JoinHandle;
use zbus::message::Type as MessageType;
use zbus::{Connection, MatchRule, MessageStream};
use super::dbus::*;
use crate::browse::{
BrowseEvent, BrowseEventReceiver, BrowseEventSender, DiscoveredService, RemovedService,
ServiceBrowseError, parse_txt_entry,
};
const SERVICE_BROWSER_INTERFACE: &str = "org.freedesktop.Avahi.ServiceBrowser";
const SERVICE_TYPE_BROWSER_INTERFACE: &str = "org.freedesktop.Avahi.ServiceTypeBrowser";
pub(crate) struct BrowseGuard {
handle: JoinHandle<()>,
}
impl Drop for BrowseGuard {
fn drop(&mut self) {
self.handle.abort();
}
}
struct AbortOnDrop(JoinHandle<()>);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
pub(crate) async fn browse_start(
service_type: &Option<String>,
domain: &Option<String>,
interface_index: Option<NonZeroU32>,
) -> Result<(BrowseEventReceiver, BrowseGuard), ServiceBrowseError> {
let conn = Connection::system().await.map_err(|err| {
ServiceBrowseError::DnsSdUnavailable(format!("failed to connect to system D-Bus: {err}"))
})?;
AvahiProxy::new(&conn).await.map_err(|err| {
ServiceBrowseError::DnsSdUnavailable(format!("failed to connect to Avahi via D-Bus: {err}"))
})?;
drop(conn);
let interface = interface_to_avahi(interface_index)?;
let domain = domain.clone().unwrap_or_default();
let (tx, rx) = unbounded_channel();
let handle = match service_type {
Some(service_type) => {
let service_type = service_type.clone();
tokio::spawn(browse_one_type(interface, service_type, domain, tx))
}
None => tokio::spawn(browse_all_types(interface, domain, tx)),
};
Ok((rx, BrowseGuard { handle }))
}
async fn connect_and_subscribe(
interface: &str,
) -> Result<(Connection, MessageStream), ServiceBrowseError> {
let conn = Connection::system().await.map_err(|err| {
ServiceBrowseError::DnsSdUnavailable(format!("failed to connect to system D-Bus: {err}"))
})?;
let rule = MatchRule::builder()
.msg_type(MessageType::Signal)
.sender("org.freedesktop.Avahi")
.and_then(|b| b.interface(interface))
.map_err(|err| ServiceBrowseError::BrowseFailed(err.to_string()))?
.build();
let messages = MessageStream::for_match_rule(rule, &conn, None)
.await
.map_err(|err| ServiceBrowseError::BrowseFailed(err.to_string()))?;
Ok((conn, messages))
}
async fn browse_all_types(interface: i32, domain: String, tx: BrowseEventSender) {
let (conn, mut messages) = match connect_and_subscribe(SERVICE_TYPE_BROWSER_INTERFACE).await {
Ok(parts) => parts,
Err(err) => {
let _ = tx.send(Err(err));
return;
}
};
let server = match AvahiProxy::new(&conn).await {
Ok(server) => server,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::DnsSdUnavailable(err.to_string())));
return;
}
};
let type_browser = match server
.service_type_browser_new(interface, AVAHI_PROTO_UNSPEC, &domain, 0)
.await
{
Ok(browser) => browser,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::BrowseFailed(format!(
"ServiceTypeBrowserNew failed: {err}"
))));
return;
}
};
let mut seen: HashSet<(String, String)> = HashSet::new();
let mut child_browsers: Vec<AbortOnDrop> = Vec::new();
while let Some(msg) = messages.next().await {
let msg = match msg {
Ok(msg) => msg,
Err(err) => {
warn!("service type browser message error: {err}");
continue;
}
};
let member = msg.header().member().map(|m| m.as_str().to_owned());
match member.as_deref() {
Some("ItemNew") => {
let (_iface, _proto, service_type, item_domain, _flags): (
i32,
i32,
String,
String,
u32,
) = match msg.body().deserialize() {
Ok(args) => args,
Err(err) => {
warn!("malformed service type ItemNew: {err}");
continue;
}
};
if seen.insert((service_type.clone(), item_domain.clone())) {
trace!("discovered service type {service_type:?} in domain {item_domain:?}");
let handle = tokio::spawn(browse_one_type(
interface,
service_type,
item_domain,
tx.clone(),
));
child_browsers.push(AbortOnDrop(handle));
}
}
Some("Failure") => {
let err: String = msg.body().deserialize().unwrap_or_default();
let _ = tx.send(Err(ServiceBrowseError::BrowseFailed(format!(
"service type browser failure: {err}"
))));
}
_ => {} }
}
let _ = type_browser.free().await;
}
async fn browse_one_type(
interface: i32,
service_type: String,
domain: String,
tx: BrowseEventSender,
) {
let (conn, mut messages) = match connect_and_subscribe(SERVICE_BROWSER_INTERFACE).await {
Ok(parts) => parts,
Err(err) => {
let _ = tx.send(Err(err));
return;
}
};
let server = match AvahiProxy::new(&conn).await {
Ok(server) => server,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::DnsSdUnavailable(err.to_string())));
return;
}
};
let browser = match server
.service_browser_new(interface, AVAHI_PROTO_UNSPEC, &service_type, &domain, 0)
.await
{
Ok(browser) => browser,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::BrowseFailed(format!(
"ServiceBrowserNew failed for {service_type}: {err}"
))));
return;
}
};
let mut resolvers: Vec<AbortOnDrop> = Vec::new();
while let Some(msg) = messages.next().await {
let msg = match msg {
Ok(msg) => msg,
Err(err) => {
warn!("service browser message error: {err}");
continue;
}
};
let member = msg.header().member().map(|m| m.as_str().to_owned());
match member.as_deref() {
Some("ItemNew") => {
let (iface, protocol, name, item_type, item_domain, _flags): (
i32,
i32,
String,
String,
String,
u32,
) = match msg.body().deserialize() {
Ok(args) => args,
Err(err) => {
warn!("malformed ItemNew: {err}");
continue;
}
};
resolvers.retain(|r| !r.0.is_finished());
let handle = tokio::spawn(resolve_and_emit(
conn.clone(),
iface,
protocol,
name,
item_type,
item_domain,
tx.clone(),
));
resolvers.push(AbortOnDrop(handle));
}
Some("ItemRemove") => {
let (iface, _protocol, name, item_type, item_domain, _flags): (
i32,
i32,
String,
String,
String,
u32,
) = match msg.body().deserialize() {
Ok(args) => args,
Err(err) => {
warn!("malformed ItemRemove: {err}");
continue;
}
};
let removed = RemovedService {
name,
service_type: item_type,
domain: item_domain,
interface_index: avahi_interface_to_index(iface),
};
if tx.send(Ok(BrowseEvent::Removed(removed))).is_err() {
break;
}
}
Some("Failure") => {
let err: String = msg.body().deserialize().unwrap_or_default();
let _ = tx.send(Err(ServiceBrowseError::BrowseFailed(format!(
"service browser failure for {service_type}: {err}"
))));
}
_ => {} }
}
let _ = browser.free().await;
}
async fn resolve_and_emit(
conn: Connection,
interface: i32,
protocol: i32,
name: String,
service_type: String,
domain: String,
tx: BrowseEventSender,
) {
let server = match AvahiProxy::new(&conn).await {
Ok(server) => server,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::ResolveFailed(
name,
err.to_string(),
)));
return;
}
};
match server
.resolve_service(
interface,
protocol,
&name,
&service_type,
&domain,
AVAHI_PROTO_UNSPEC,
0,
)
.await
{
Ok((
iface,
_proto,
name,
service_type,
domain,
host,
_aproto,
address,
port,
txt,
_flags,
)) => {
let addresses: Vec<IpAddr> = IpAddr::from_str(&address).ok().into_iter().collect();
let txt_records = txt.iter().map(|entry| parse_txt_entry(entry)).collect();
let service = DiscoveredService {
name,
service_type,
domain,
host_name: host,
port,
addresses,
txt_records,
interface_index: avahi_interface_to_index(iface),
};
let _ = tx.send(Ok(BrowseEvent::Found(service)));
}
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::ResolveFailed(
name,
err.to_string(),
)));
}
}
}
fn interface_to_avahi(interface_index: Option<NonZeroU32>) -> Result<i32, ServiceBrowseError> {
match interface_index {
Some(i) => {
let idx = i.get();
if idx > i32::MAX as u32 {
return Err(ServiceBrowseError::InvalidInterfaceIndex(idx));
}
Ok(idx as i32)
}
None => Ok(AVAHI_IF_UNSPEC),
}
}
fn avahi_interface_to_index(interface: i32) -> Option<NonZeroU32> {
if interface <= 0 {
None
} else {
NonZeroU32::new(interface as u32)
}
}