async_zeroconf/
service.rs

1use crate::{
2    BonjourError, Interface, OpKind, OpType, ProcessTask, ServiceRef, ServiceRefWrapper, TxtRecord,
3    ZeroconfError,
4};
5use std::{ffi, fmt};
6use tokio::sync::mpsc;
7
8use bonjour_sys::{DNSServiceErrorType, DNSServiceFlags, DNSServiceRef};
9use std::future::Future;
10use std::sync::Arc;
11
12#[derive(Debug)]
13struct ServicePublishContext {
14    tx: mpsc::UnboundedSender<Result<(), BonjourError>>,
15}
16
17impl ServicePublishContext {
18    fn send(&self, e: Result<(), BonjourError>) {
19        if let Err(e) = self.tx.send(e) {
20            log::warn!("Failed to send status, receiver dropped: {}", e);
21        }
22    }
23}
24
25/// Struct representing a `ZeroConf` service. This should be created with all
26/// the information that should be associated with the service and then the
27/// [`publish`][`Service::publish`] method can be used to register the service.
28/// The [`ServiceRef`] returned from [`publish`][`Service::publish`] should be held
29/// for as long as the service should continue being advertised, once dropped
30/// the service will be deallocated.
31///
32/// # Examples
33///
34/// Normally the default values of `domain`, `host` and `interface` don't need
35/// to be changed.
36/// ```
37/// # tokio_test::block_on(async {
38/// let service_ref = async_zeroconf::Service::new("Server", "_http._tcp", 80)
39///                       .publish().await?;
40/// // Service kept alive until service_ref dropped
41/// # Ok::<(), async_zeroconf::ZeroconfError>(())
42/// # });
43/// ```
44///
45/// These fields can be customised if required. More details are available in
46/// the [`DNSServiceRegister`][reg] documentation.
47/// ```
48/// # tokio_test::block_on(async {
49/// let service_ref = async_zeroconf::Service::new("Server", "_http._tcp", 80)
50///                       .set_domain("local".to_string())
51///                       .set_host("localhost".to_string())
52///                       .publish().await?;
53/// // Service kept alive until service_ref dropped
54/// # Ok::<(), async_zeroconf::ZeroconfError>(())
55/// # });
56/// ```
57/// [reg]: https://developer.apple.com/documentation/dnssd/1804733-dnsserviceregister?language=objc
58#[derive(Debug, Clone, Eq, PartialEq)]
59pub struct Service {
60    name: String,
61    service_type: String,
62    port: u16,
63    interface: Interface,
64    domain: Option<String>,
65    host: Option<String>,
66    txt: TxtRecord,
67    browse: bool,
68    resolve: bool,
69    allow_rename: bool,
70}
71
72impl fmt::Display for Service {
73    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74        let host_fmt = self.host.as_deref().unwrap_or("*");
75        let txt = if self.txt.is_empty() {
76            "".to_string()
77        } else {
78            format!(" {}", self.txt)
79        };
80        write!(
81            f,
82            "[{}:{}] @{}:{}{}",
83            self.name, self.service_type, host_fmt, self.port, txt
84        )
85    }
86}
87
88// Callback passed to DNSServiceRegister
89unsafe extern "C" fn dns_sd_callback(
90    _sd_ref: DNSServiceRef,
91    _flags: DNSServiceFlags,
92    error: DNSServiceErrorType,
93    name: *const libc::c_char,
94    regtype: *const libc::c_char,
95    domain: *const libc::c_char,
96    context: *mut libc::c_void,
97) {
98    let proxy = &*(context as *const ServicePublishContext);
99    if error == 0 {
100        let c_name = ffi::CStr::from_ptr(name);
101        let c_type = ffi::CStr::from_ptr(regtype);
102        let c_domain = ffi::CStr::from_ptr(domain);
103        let name = c_name
104            .to_str()
105            .expect("string originally from rust should be safe");
106        let regtype = c_type
107            .to_str()
108            .expect("string originally from rust should be safe");
109        let domain = c_domain
110            .to_str()
111            .expect("string originally from rust should be safe");
112        log::debug!("Service Callback OK ({}:{}:{})", name, regtype, domain);
113        proxy.send(Ok(()));
114    } else {
115        log::debug!(
116            "Service Callback Error ({}:{})",
117            error,
118            Into::<BonjourError>::into(error)
119        );
120        proxy.send(Err(error.into()));
121    }
122}
123
124impl Service {
125    /// Create a new Service, called `name` of type `service_type` that is
126    /// listening on port `port`.
127    ///
128    /// This must then be published with [`Service::publish`] to advertise the
129    /// service.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// // Create a service description
135    /// let service = async_zeroconf::Service::new("Web Server", "_http._tcp", 80);
136    /// ```
137    pub fn new(name: &str, service_type: &str, port: u16) -> Self {
138        Service::new_with_txt(name, service_type, port, TxtRecord::new())
139    }
140
141    /// Create a new Service, called `name` of type `service_type` that is
142    /// listening on port `port` with the TXT records described by `txt`.
143    ///
144    /// This must then be published with [`Service::publish`] to advertise the
145    /// service.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// // Create a TXT record collection
151    /// let mut txt = async_zeroconf::TxtRecord::new();
152    /// txt.add("version".to_string(), "0.1".to_string());
153    /// // Create a service description
154    /// let service = async_zeroconf::Service::new_with_txt("Web Server", "_http._tcp", 80, txt);
155    /// ```
156    pub fn new_with_txt(name: &str, service_type: &str, port: u16, txt: TxtRecord) -> Self {
157        Service {
158            name: name.to_string(),
159            service_type: service_type.to_string(),
160            port,
161            interface: Default::default(),
162            domain: None,
163            host: None,
164            txt,
165            browse: false,
166            resolve: false,
167            allow_rename: true,
168        }
169    }
170
171    fn validate_service_type(&self) -> bool {
172        if self.service_type.contains('.') {
173            let parts: Vec<&str> = self.service_type.split('.').collect();
174            if parts[0].starts_with('_') && (parts[1] == "_udp" || parts[1] == "_tcp") {
175                return true;
176            }
177        }
178        false
179    }
180
181    fn validate(&self) -> Result<(), ZeroconfError> {
182        if self.validate_service_type() {
183            self.txt.validate()
184        } else {
185            Err(ZeroconfError::InvalidServiceType(self.service_type.clone()))
186        }
187    }
188
189    /// Set an interface to advertise the service on rather than all.
190    ///
191    /// By default the service will be advertised on all interfaces.
192    pub fn set_interface(&mut self, interface: Interface) -> &mut Self {
193        self.interface = interface;
194        self
195    }
196
197    /// Get this interface associated with this service
198    pub fn interface(&self) -> &Interface {
199        &self.interface
200    }
201
202    /// Prevent renaming of this service if there is a name collision.
203    ///
204    /// By default the service will be automatically renamed.
205    pub fn prevent_rename(&mut self) -> &mut Self {
206        self.allow_rename = false;
207        self
208    }
209
210    /// Set the (optional) domain for the service.
211    ///
212    /// If not specified, the default domain is used.
213    pub fn set_domain(&mut self, domain: String) -> &mut Self {
214        self.domain = Some(domain);
215        self
216    }
217
218    /// Get the domain of this service
219    pub fn domain(&self) -> &Option<String> {
220        &self.domain
221    }
222
223    /// Set the (optional) hostname for the service.
224    ///
225    /// If not set, the hostname of the host will be used.
226    pub fn set_host(&mut self, host: String) -> &mut Self {
227        self.host = Some(host);
228        self
229    }
230
231    /// Set the from browse flag for this service
232    pub(crate) fn set_browse(&mut self) -> &mut Self {
233        self.browse = true;
234        self
235    }
236
237    /// Set the from resolve flag for this service
238    pub(crate) fn set_resolve(&mut self) -> &mut Self {
239        self.resolve = true;
240        self
241    }
242
243    /// Get the name of the service
244    pub fn name(&self) -> &str {
245        &self.name
246    }
247
248    /// Get the type of the service
249    pub fn service_type(&self) -> &str {
250        &self.service_type
251    }
252
253    /// Get the port of the service
254    pub fn port(&self) -> u16 {
255        self.port
256    }
257
258    /// Get the host of the service
259    pub fn host(&self) -> &Option<String> { &self.host }
260
261    /// Get the TxtRecord for this service
262    pub fn txt(&self) -> &TxtRecord { &self.txt }
263
264    /// Add a TXT entry to the service
265    pub fn add_txt(&mut self, k: String, v: String) -> &mut Self {
266        self.txt.add(k, v);
267        self
268    }
269
270    /// Get the browse flag
271    pub(crate) fn browse(&self) -> bool {
272        self.browse
273    }
274
275    /// Get the resolve flag
276    pub(crate) fn resolve(&self) -> bool {
277        self.resolve
278    }
279
280    /// Publish the service, returns a [`ServiceRef`] which should be held to
281    /// keep the service alive. Once the [`ServiceRef`] is dropped the service
282    /// will be removed and deallocated.
283    ///
284    /// # Arguments
285    ///
286    /// * `allow_rename` - Allow the service to be automatically renamed if
287    /// a service with the same name already exists
288    ///
289    /// # Examples
290    /// ```
291    /// # tokio_test::block_on(async {
292    /// // Create a service description
293    /// let service = async_zeroconf::Service::new("Server", "_http._tcp", 80);
294    /// // Publish the service
295    /// let service_ref = service.publish().await?;
296    /// // Service kept alive until service_ref dropped
297    /// # Ok::<(), async_zeroconf::ZeroconfError>(())
298    /// # });
299    /// ```
300    pub async fn publish(&self) -> Result<ServiceRef, ZeroconfError> {
301        let (service, task, future) = self.publish_task()?;
302
303        // Spawn task
304        tokio::spawn(task);
305
306        // Get any errors and wait until service started
307        future.await?;
308
309        Ok(service)
310    }
311
312    /// Publish the service, returns a [`ServiceRef`] which should be held to
313    /// keep the service alive and a future which should be awaited on to
314    /// respond to any events associated with keeping the service registered.
315    /// Once the [`ServiceRef`] is dropped the service will be removed and
316    /// deallocated.
317    ///
318    /// # Note
319    /// This method is intended if more control is needed over how the task
320    /// is spawned. [`Service::publish`] will automatically spawn the task.
321    /// The task should be spawned first to process events, and then the
322    /// returned future waited on to collect any errors that occurred.
323    ///
324    /// # Examples
325    /// ```
326    /// # tokio_test::block_on(async {
327    /// // Create a service description
328    /// let service = async_zeroconf::Service::new("Server", "_http._tcp", 80);
329    /// // Publish the service
330    /// let (service_ref, task, service_ok) = service.publish_task()?;
331    /// // Spawn the task to respond to events
332    /// tokio::spawn(task);
333    /// // Wait to confirm service started ok
334    /// service_ok.await?;
335    /// // Service kept alive until service_ref dropped
336    /// # Ok::<(), async_zeroconf::ZeroconfError>(())
337    /// # });
338    /// ```
339    pub fn publish_task(
340        &self,
341    ) -> Result<
342        (
343            ServiceRef,
344            impl ProcessTask,
345            impl Future<Output = Result<(), ZeroconfError>>,
346        ),
347        ZeroconfError,
348    > {
349        self.validate()?;
350
351        let (tx, mut rx) = mpsc::unbounded_channel();
352
353        let callback_context = ServicePublishContext { tx };
354        let context = Arc::new(callback_context);
355        let context_ptr =
356            Arc::as_ptr(&context) as *mut Arc<ServicePublishContext> as *mut libc::c_void;
357
358        let service_ref = crate::c_intf::service_register(
359            (&self.name, &self.service_type, self.port),
360            &self.interface,
361            (self.domain.as_deref(), self.host.as_deref()),
362            &self.txt,
363            Some(dns_sd_callback),
364            self.allow_rename,
365            context_ptr,
366        )?;
367
368        let (r, task) = ServiceRefWrapper::from_service(
369            service_ref,
370            OpType::new(&self.service_type, OpKind::Publish),
371            Some(Box::new(context)),
372            None,
373        )?;
374
375        let fut = async move {
376            match rx.recv().await {
377                Some(v) => v.map_err(|e| e.into()),
378                None => Err(ZeroconfError::Dropped),
379            }
380        };
381
382        Ok((r, task, fut))
383    }
384}