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;