diff --git a/examples/browser/src/main.rs b/examples/browser/src/main.rs index 0797c83..5f768ce 100644 --- a/examples/browser/src/main.rs +++ b/examples/browser/src/main.rs @@ -7,7 +7,7 @@ use std::any::Any; use std::sync::Arc; use std::time::Duration; use zeroconf::prelude::*; -use zeroconf::{MdnsBrowser, ServiceDiscovery, ServiceType}; +use zeroconf::{BrowserEvent, MdnsBrowser, ServiceType}; /// Example of a simple mDNS browser #[derive(Parser, Debug)] @@ -27,7 +27,7 @@ struct Args { } fn main() -> zeroconf::Result<()> { - env_logger::init(); + env_logger::Builder::from_env(env_logger::Env::new().filter_or("RUST_LOG", "info")).init(); let Args { name, @@ -45,7 +45,7 @@ fn main() -> zeroconf::Result<()> { let mut browser = MdnsBrowser::new(service_type); - browser.set_service_discovered_callback(Box::new(on_service_discovered)); + browser.set_service_callback(Box::new(on_service_discovery_event)); let event_loop = browser.browse_services()?; @@ -55,12 +55,12 @@ fn main() -> zeroconf::Result<()> { } } -fn on_service_discovered( - result: zeroconf::Result, +fn on_service_discovery_event( + result: zeroconf::Result, _context: Option>, ) { info!( - "Service discovered: {:?}", + "Service event: {:?}", result.expect("service discovery failed") ); diff --git a/zeroconf/src/avahi/browser.rs b/zeroconf/src/avahi/browser.rs index f041178..6adab5e 100644 --- a/zeroconf/src/avahi/browser.rs +++ b/zeroconf/src/avahi/browser.rs @@ -14,8 +14,8 @@ use crate::ffi::{c_str, AsRaw, FromRaw}; use crate::prelude::*; use crate::Result; use crate::{ - EventLoop, NetworkInterface, ServiceDiscoveredCallback, ServiceDiscovery, ServiceType, - TxtRecord, + BrowserEvent, EventLoop, NetworkInterface, ServiceBrowserCallback, ServiceDiscovery, + ServiceRemoval, ServiceType, TxtRecord, }; use avahi_sys::{ AvahiAddress, AvahiBrowserEvent, AvahiClient, AvahiClientFlags, AvahiClientState, AvahiIfIndex, @@ -56,11 +56,8 @@ impl TMdnsBrowser for AvahiMdnsBrowser { avahi_util::interface_from_index(self.context.interface_index) } - fn set_service_discovered_callback( - &mut self, - service_discovered_callback: Box, - ) { - self.context.service_discovered_callback = Some(service_discovered_callback); + fn set_service_callback(&mut self, service_callback: Box) { + self.context.service_callback = Some(service_callback); } fn set_context(&mut self, context: Box) { @@ -112,7 +109,7 @@ impl TMdnsBrowser for AvahiMdnsBrowser { struct AvahiBrowserContext { client: Option>, resolvers: ServiceResolverSet, - service_discovered_callback: Option>, + service_callback: Option>, user_context: Option>, interface_index: AvahiIfIndex, kind: CString, @@ -124,7 +121,7 @@ impl AvahiBrowserContext { Self { client: None, resolvers: ServiceResolverSet::default(), - service_discovered_callback: None, + service_callback: None, user_context: None, interface_index, kind, @@ -132,8 +129,8 @@ impl AvahiBrowserContext { } } - fn invoke_callback(&self, result: Result) { - if let Some(f) = &self.service_discovered_callback { + fn invoke_callback(&self, result: Result) { + if let Some(f) = &self.service_callback { f(result, self.user_context.clone()); } else { warn!("attempted to invoke browser callback but none was set"); @@ -205,6 +202,9 @@ unsafe extern "C" fn browse_callback( avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_FAILURE => { context.invoke_callback(Err("browser failure".into())) } + avahi_sys::AvahiBrowserEvent_AVAHI_BROWSER_REMOVE => { + handle_browser_remove(context, name, kind, domain); + } _ => {} }; } @@ -242,6 +242,26 @@ unsafe fn handle_browser_new( Ok(()) } +unsafe fn handle_browser_remove( + ctx: &mut AvahiBrowserContext, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, +) { + let name = c_str::raw_to_str(name); + let regtype = c_str::raw_to_str(regtype); + let domain = c_str::raw_to_str(domain); + + ctx.invoke_callback(Ok(BrowserEvent::Remove( + ServiceRemoval::builder() + .name(name.to_string()) + .kind(regtype.to_string()) + .domain(domain.to_string()) + .build() + .expect("could not build ServiceRemoval"), + ))); +} + unsafe extern "C" fn resolve_callback( resolver: *mut AvahiServiceResolver, _interface: AvahiIfIndex, @@ -324,7 +344,7 @@ unsafe fn handle_resolver_found( debug!("Service resolved: {:?}", result); - context.invoke_callback(Ok(result)); + context.invoke_callback(Ok(BrowserEvent::Add(result))); Ok(()) } diff --git a/zeroconf/src/bonjour/browser.rs b/zeroconf/src/bonjour/browser.rs index d410b3f..20ea4b5 100644 --- a/zeroconf/src/bonjour/browser.rs +++ b/zeroconf/src/bonjour/browser.rs @@ -7,8 +7,8 @@ use super::txt_record_ref::ManagedTXTRecordRef; use super::{bonjour_util, constants}; use crate::ffi::{c_str, AsRaw, FromRaw}; use crate::prelude::*; +use crate::{BrowserEvent, ServiceBrowserCallback, ServiceDiscovery, ServiceRemoval}; use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; -use crate::{ServiceDiscoveredCallback, ServiceDiscovery}; #[cfg(target_vendor = "pc")] use bonjour_sys::sockaddr_in; use bonjour_sys::{DNSServiceErrorType, DNSServiceFlags, DNSServiceRef}; @@ -48,10 +48,7 @@ impl TMdnsBrowser for BonjourMdnsBrowser { bonjour_util::interface_from_index(self.interface_index) } - fn set_service_discovered_callback( - &mut self, - service_discovered_callback: Box, - ) { + fn set_service_callback(&mut self, service_discovered_callback: Box) { self.context.service_discovered_callback = Some(service_discovered_callback); } @@ -88,7 +85,7 @@ impl TMdnsBrowser for BonjourMdnsBrowser { #[derive(Default, FromRaw, AsRaw)] struct BonjourBrowserContext { - service_discovered_callback: Option>, + service_discovered_callback: Option>, resolved_name: Option, resolved_kind: Option, resolved_domain: Option, @@ -98,7 +95,7 @@ struct BonjourBrowserContext { } impl BonjourBrowserContext { - fn invoke_callback(&self, result: Result) { + fn invoke_callback(&self, result: Result) { if let Some(f) = &self.service_discovered_callback { f(result, self.user_context.clone()); } else { @@ -120,7 +117,7 @@ impl fmt::Debug for BonjourBrowserContext { unsafe extern "system" fn browse_callback( _sd_ref: DNSServiceRef, - _flags: DNSServiceFlags, + flags: DNSServiceFlags, interface_index: u32, error: DNSServiceErrorType, name: *const c_char, @@ -129,23 +126,32 @@ unsafe extern "system" fn browse_callback( context: *mut c_void, ) { let ctx = BonjourBrowserContext::from_raw(context); - if let Err(e) = handle_browse(ctx, error, name, regtype, domain, interface_index) { - ctx.invoke_callback(Err(e)); + + if error != 0 { + ctx.invoke_callback(Err(format!( + "browse_callback() reported error (code: {})", + error + ) + .into())); + return; + } + + if flags & bonjour_sys::kDNSServiceFlagsAdd != 0 { + if let Err(e) = handle_browse_add(ctx, name, regtype, domain, interface_index) { + ctx.invoke_callback(Err(e)); + } + } else { + handle_browse_remove(ctx, name, regtype, domain); } } -unsafe fn handle_browse( +unsafe fn handle_browse_add( ctx: &mut BonjourBrowserContext, - error: DNSServiceErrorType, name: *const c_char, regtype: *const c_char, domain: *const c_char, interface_index: u32, ) -> Result<()> { - if error != 0 { - return Err(format!("browse_callback() reported error (code: {})", error).into()); - } - ctx.resolved_name = Some(c_str::copy_raw(name)); ctx.resolved_kind = Some(c_str::copy_raw(regtype)); ctx.resolved_domain = Some(c_str::copy_raw(domain)); @@ -163,6 +169,30 @@ unsafe fn handle_browse( ) } +unsafe fn handle_browse_remove( + ctx: &mut BonjourBrowserContext, + name: *const c_char, + regtype: *const c_char, + domain: *const c_char, +) { + let name = c_str::raw_to_str(name); + let regtype = c_str::raw_to_str(regtype); + let domain = c_str::raw_to_str(domain); + + // Remove the "." suffix to be consistent with the Avahi implementation. + let regtype = regtype.strip_suffix(".").unwrap_or(domain); + let domain = domain.strip_suffix(".").unwrap_or(domain); + + ctx.invoke_callback(Ok(BrowserEvent::Remove( + ServiceRemoval::builder() + .name(name.to_string()) + .kind(regtype.to_string()) + .domain(domain.to_string()) + .build() + .expect("could not build ServiceRemoval"), + ))); +} + unsafe extern "system" fn resolve_callback( _sd_ref: DNSServiceRef, _flags: DNSServiceFlags, @@ -313,7 +343,7 @@ unsafe fn handle_get_address_info( .build() .expect("could not build ServiceResolution"); - ctx.invoke_callback(Ok(result)); + ctx.invoke_callback(Ok(BrowserEvent::Add(result))); Ok(()) } diff --git a/zeroconf/src/browser.rs b/zeroconf/src/browser.rs index 4561a31..5817e7d 100644 --- a/zeroconf/src/browser.rs +++ b/zeroconf/src/browser.rs @@ -4,6 +4,15 @@ use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; use std::any::Any; use std::sync::Arc; +/// Event from [`MdnsBrowser`] received by the `ServiceBrowserCallback`. +/// +/// [`MdnsBrowser`]: type.MdnsBrowser.html +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BrowserEvent { + Add(ServiceDiscovery), + Remove(ServiceRemoval), +} + /// Interface for interacting with underlying mDNS implementation service browsing capabilities. pub trait TMdnsBrowser { /// Creates a new `MdnsBrowser` that browses for the specified `kind` (e.g. `_http._tcp`) @@ -18,14 +27,11 @@ pub trait TMdnsBrowser { /// Returns the network interface on which to browse for services on. fn network_interface(&self) -> NetworkInterface; - /// Sets the [`ServiceDiscoveredCallback`] that is invoked when the browser has discovered and - /// resolved a service. + /// Sets the [`ServiceBrowserCallback`] that is invoked when the browser has discovered and + /// resolved or removed a service. /// - /// [`ServiceDiscoveredCallback`]: ../type.ServiceDiscoveredCallback.html - fn set_service_discovered_callback( - &mut self, - service_discovered_callback: Box, - ); + /// [`ServiceBrowserCallback`]: ../type.ServiceBrowserCallback.html + fn set_service_callback(&mut self, service_callback: Box); /// Sets the optional user context to pass through to the callback. This is useful if you need /// to share state between pre and post-callback. The context type must implement `Any`. @@ -38,14 +44,15 @@ pub trait TMdnsBrowser { fn browse_services(&mut self) -> Result; } -/// Callback invoked from [`MdnsBrowser`] once a service has been discovered and resolved. +/// Callback invoked from [`MdnsBrowser`] once a service has been discovered and resolved or +/// removed. /// /// # Arguments -/// * `discovered_service` - The service that was disovered +/// * `browser_event` - The event received from Zeroconf /// * `context` - The optional user context passed through /// /// [`MdnsBrowser`]: type.MdnsBrowser.html -pub type ServiceDiscoveredCallback = dyn Fn(Result, Option>); +pub type ServiceBrowserCallback = dyn Fn(Result, Option>); /// Represents a service that has been discovered by a [`MdnsBrowser`]. /// @@ -61,3 +68,16 @@ pub struct ServiceDiscovery { port: u16, txt: Option, } + +/// Represents a service that has been removed by a [`MdnsBrowser`]. +/// +/// [`MdnsBrowser`]: type.MdnsBrowser.html +#[derive(Debug, Getters, Builder, BuilderDelegate, Clone, PartialEq, Eq)] +pub struct ServiceRemoval { + /// The "abc" part in "abc._http._udp.local" + name: String, + /// The "_http._udp" part in "abc._http._udp.local" + kind: String, + /// The "local" part in "abc._http._udp.local" + domain: String, +} diff --git a/zeroconf/src/lib.rs b/zeroconf/src/lib.rs index ad88d21..6263f21 100644 --- a/zeroconf/src/lib.rs +++ b/zeroconf/src/lib.rs @@ -116,7 +116,7 @@ //! use std::sync::Arc; //! use std::time::Duration; //! use zeroconf::prelude::*; -//! use zeroconf::{MdnsBrowser, ServiceDiscovery, ServiceType}; +//! use zeroconf::{BrowserEvent, MdnsBrowser, ServiceDiscovery, ServiceRemoval, ServiceType}; //! //! /// Example of a simple mDNS browser //! #[derive(Parser, Debug)] @@ -154,7 +154,7 @@ //! //! let mut browser = MdnsBrowser::new(service_type); //! -//! browser.set_service_discovered_callback(Box::new(on_service_discovered)); +//! browser.set_service_callback(Box::new(on_service_event)); //! //! let event_loop = browser.browse_services()?; //! @@ -164,12 +164,12 @@ //! } //! } //! -//! fn on_service_discovered( -//! result: zeroconf::Result, +//! fn on_service_event( +//! result: zeroconf::Result, //! _context: Option>, //! ) { //! info!( -//! "Service discovered: {:?}", +//! "Service event: {:?}", //! result.expect("service discovery failed") //! ); //! @@ -228,7 +228,7 @@ pub mod avahi; #[cfg(any(target_vendor = "apple", target_vendor = "pc"))] pub mod bonjour; -pub use browser::{ServiceDiscoveredCallback, ServiceDiscovery}; +pub use browser::{BrowserEvent, ServiceBrowserCallback, ServiceDiscovery, ServiceRemoval}; pub use interface::*; pub use service::{ServiceRegisteredCallback, ServiceRegistration}; pub use service_type::*; diff --git a/zeroconf/src/tests/service_test.rs b/zeroconf/src/tests/service_test.rs index 916d0d9..055f044 100644 --- a/zeroconf/src/tests/service_test.rs +++ b/zeroconf/src/tests/service_test.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{MdnsBrowser, MdnsService, ServiceType, TxtRecord}; +use crate::{BrowserEvent, MdnsBrowser, MdnsService, ServiceType, TxtRecord}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -46,22 +46,30 @@ fn service_register_is_browsable() { browser.set_context(Box::new(context.clone())); - browser.set_service_discovered_callback(Box::new(|service, context| { - let service = service.unwrap(); - - if service.name() == SERVICE_NAME { - let mut mtx = context - .as_ref() - .unwrap() - .downcast_ref::>>() - .unwrap() - .lock() - .unwrap(); - - mtx.txt.clone_from(service.txt()); - mtx.is_discovered = true; - - debug!("Service discovered"); + browser.set_service_callback(Box::new(|event, context| match event.unwrap() { + BrowserEvent::Add(service) => { + if service.name() == SERVICE_NAME { + let mut mtx = context + .as_ref() + .unwrap() + .downcast_ref::>>() + .unwrap() + .lock() + .unwrap(); + + mtx.txt.clone_from(service.txt()); + mtx.is_discovered = true; + + debug!("Service discovered"); + } + } + BrowserEvent::Remove(service) => { + debug!( + "Service removed: {}.{}.{}", + service.name(), + service.kind(), + service.domain() + ); } }));