Skip to main content

ebus_discovery_tokio/
lib.rs

1//! mDNS broker discovery and host advertisement for eBus on tokio.
2//!
3//! The protocol decisions live in the sans-IO [`ebus_discovery`] crate; this
4//! crate runs them over an mDNS stack. A host makes one [`Mdns`] and uses it
5//! for its two jobs:
6//!
7//! | Job | Call |
8//! |---|---|
9//! | Find the site's broker before building its MQTT options | [`find_broker`], or [`list_brokers`] to pick from every broker seen |
10//! | Find the same broker again after its address changes | [`reresolve`] |
11//! | Advertise the host's own services | [`advertise`], then [`Advertiser::update`] and [`Advertiser::withdraw`] |
12//!
13//! The backends implement [`Backend`]: Avahi under the `avahi` feature, a
14//! client of avahi-daemon over D-Bus that compiles only on Linux, and
15//! mdns-sd under the `mdns-sd` feature, with its own responder thread and
16//! sockets. [`Mdns::system`] picks the backend at run time. Built with
17//! neither feature, [`Mdns::system`] returns [`Error::NoBackend`]; a host can
18//! still pass its own [`Backend`] to the search and advertising functions.
19//!
20//! ```no_run
21//! # #[cfg(feature = "mdns-sd")]
22//! # async fn host() -> Result<(), Box<dyn std::error::Error>> {
23//! use ebus_discovery_tokio::ebus_discovery::{BrokerConfig, BrokerEndpoint, BrokerMode};
24//! use ebus_discovery_tokio::{Mdns, find_broker};
25//!
26//! let mdns = Mdns::system().await?;
27//! let mut cfg = BrokerConfig::default();
28//! cfg.mode = Some(BrokerMode::DiscoveryWithFallback);
29//! cfg.url = Some(BrokerEndpoint::from_url("mqtt://broker.example:1883")?);
30//! let broker = find_broker(&mdns, cfg).await?;
31//! let (host, port) = (broker.connect_host().into_owned(), broker.port);
32//! # let _ = (host, port);
33//! # Ok(())
34//! # }
35//! ```
36
37#![cfg_attr(docsrs, feature(doc_cfg))]
38
39mod advertiser;
40#[cfg(all(feature = "avahi", target_os = "linux"))]
41mod avahi;
42mod backend;
43#[cfg(test)]
44mod fake;
45mod mdns;
46#[cfg(feature = "mdns-sd")]
47mod mdns_sd;
48mod search;
49
50pub use ebus_discovery;
51
52pub use advertiser::{AdState, Advertiser, advertise};
53pub use backend::{Backend, Browse, Group, GroupReport, HostName};
54#[cfg(feature = "mdns-sd")]
55pub use mdns::MdnsSdOptions;
56pub use mdns::{Mdns, MdnsBrowse, MdnsGroup};
57pub use search::{find_broker, list_brokers, reresolve};
58
59use core::fmt;
60
61use ebus_discovery::{BrokerConfigError, TxtError};
62
63/// A discovery or advertising failure.
64#[derive(Debug)]
65#[non_exhaustive]
66pub enum Error {
67    /// No backend is compiled in, or none is usable on this host.
68    NoBackend,
69    /// No D-Bus system bus to reach Avahi over.
70    NoSystemBus,
71    /// avahi-daemon is not running or not reachable.
72    AvahiUnavailable,
73    /// The call needs a tokio runtime and none is running.
74    NoRuntime,
75    /// A broker configuration a search cannot run with.
76    Config(BrokerConfigError),
77    /// An advertisement that cannot be built.
78    Txt(TxtError),
79    /// A host name the backend cannot advertise under.
80    HostName(String),
81    /// The backend refused or failed an operation.
82    Backend(String),
83    /// The backend, or the advertiser's task, has stopped.
84    Closed,
85}
86
87impl fmt::Display for Error {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Self::NoBackend => f.write_str("no mDNS backend is available"),
91            Self::NoSystemBus => f.write_str("no D-Bus system bus"),
92            Self::AvahiUnavailable => f.write_str("avahi-daemon is not reachable"),
93            Self::NoRuntime => f.write_str("no tokio runtime is running"),
94            Self::Config(_) => f.write_str("invalid broker configuration"),
95            Self::Txt(_) => f.write_str("invalid advertisement"),
96            Self::HostName(e) => write!(f, "host name: {e}"),
97            Self::Backend(e) => write!(f, "mDNS backend: {e}"),
98            Self::Closed => f.write_str("the mDNS backend has stopped"),
99        }
100    }
101}
102
103impl std::error::Error for Error {
104    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
105        match self {
106            Self::Config(e) => Some(e),
107            Self::Txt(e) => Some(e),
108            _ => None,
109        }
110    }
111}
112
113impl From<BrokerConfigError> for Error {
114    fn from(e: BrokerConfigError) -> Self {
115        Self::Config(e)
116    }
117}
118
119impl From<TxtError> for Error {
120    fn from(e: TxtError) -> Self {
121        Self::Txt(e)
122    }
123}
124
125/// A std mutex guard; a poisoned lock yields its data.
126pub(crate) fn lock<T>(m: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
127    m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
128}
129
130#[cfg(test)]
131mod tests {
132    use std::error::Error as _;
133
134    use super::*;
135
136    #[test]
137    fn wrapped_errors_are_reported_once() {
138        for (e, inner) in [
139            (
140                Error::from(BrokerConfigError::NoUrl),
141                BrokerConfigError::NoUrl.to_string(),
142            ),
143            (
144                Error::from(TxtError::NoDeviceId),
145                TxtError::NoDeviceId.to_string(),
146            ),
147        ] {
148            assert!(!e.to_string().contains(&inner), "{e}");
149            assert_eq!(e.source().map(ToString::to_string), Some(inner));
150        }
151    }
152}
153
154#[cfg(doctest)]
155#[doc = include_str!("../README.md")]
156struct ReadmeDoctests;