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}