#![allow(
// Service / event names duplicate across docs and code; no gain
// from backtick-wrapping every mention.
clippy::doc_markdown,
// `AdvertiseOptions` is moved-in deliberately; Browser & Advertiser
// both have owned state paths that don't consume the input fully.
clippy::needless_pass_by_value,
// Folding `ServiceEvent::SearchStarted | SearchStopped | ServiceFound => None`
// with the `_ => None` catch-all loses the explicit variant list we
// want future-readers to see.
clippy::match_same_arms
)]
mod advertiser;
mod browser;
mod error;
mod txt;
pub use advertiser::{AdvertiseOptions, Advertiser, local_hostname};
pub use browser::{Browser, DiscoveredServer, DiscoveryEvent};
pub use error::DiscoveryError;
pub use txt::TxtRecord;
pub const SERVICE_TYPE: &str = "_rtl_tcp._tcp.local.";
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn local_hostname_returns_bare_non_empty_name_without_local_suffix() {
let host = local_hostname();
assert!(!host.is_empty(), "local_hostname() returned empty string");
#[allow(clippy::case_sensitive_file_extension_comparisons)]
let ends_bad = host.ends_with(".local.") || host.ends_with(".local");
assert!(
!ends_bad,
"local_hostname() must return bare name, not mDNS-qualified: {host:?}"
);
assert!(!host.contains('\0'));
}
#[test]
fn service_type_matches_dns_sd_shape() {
assert_eq!(SERVICE_TYPE, "_rtl_tcp._tcp.local.");
assert!(SERVICE_TYPE.starts_with("_rtl_tcp."));
assert!(SERVICE_TYPE.contains("._tcp."));
assert!(SERVICE_TYPE.ends_with("local."));
}
#[test]
#[ignore = "needs multicast network; run with --ignored locally"]
fn mdns_roundtrip() {
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const MDNS_ROUNDTRIP_PORT: u16 = 31_234;
const MDNS_PROPAGATION_TIMEOUT: Duration = Duration::from_secs(5);
const MDNS_POLL_INTERVAL: Duration = Duration::from_millis(100);
const R820T_GAIN_COUNT: u32 = 29;
let observed: Arc<Mutex<Vec<DiscoveredServer>>> = Arc::new(Mutex::new(Vec::new()));
let obs_clone = observed.clone();
let browser = Browser::start(move |event| {
if let DiscoveryEvent::ServerAnnounced(s) = event
&& s.instance_name.contains("sdr-rtltcp-integration-test")
{
obs_clone.lock().unwrap().push(s);
}
})
.expect("start browser");
let _advertiser = Advertiser::announce(AdvertiseOptions {
port: MDNS_ROUNDTRIP_PORT,
instance_name: "sdr-rtltcp-integration-test".into(),
hostname: String::new(),
txt: TxtRecord {
tuner: "R820T".into(),
version: env!("CARGO_PKG_VERSION").into(),
gains: R820T_GAIN_COUNT,
nickname: "integration-test-nick".into(),
txbuf: None,
codecs: None,
auth_required: None,
},
})
.expect("announce");
let deadline = Instant::now() + MDNS_PROPAGATION_TIMEOUT;
while Instant::now() < deadline {
if !observed.lock().unwrap().is_empty() {
break;
}
std::thread::sleep(MDNS_POLL_INTERVAL);
}
browser.stop();
let seen = observed.lock().unwrap();
assert!(
!seen.is_empty(),
"browser never observed the advertised service"
);
let server = &seen[0];
assert_eq!(server.port, MDNS_ROUNDTRIP_PORT);
assert_eq!(server.txt.tuner, "R820T");
assert_eq!(server.txt.nickname, "integration-test-nick");
assert_eq!(server.txt.gains, R820T_GAIN_COUNT);
}
}