use std::net::SocketAddr;
mod r#static;
pub use r#static::StaticBackend;
#[cfg(feature = "discovery-dns-update")]
mod dns_update;
#[cfg(feature = "discovery")]
mod dnssd;
#[cfg(feature = "discovery")]
mod mdns;
#[cfg(feature = "discovery")]
mod zone;
#[cfg(feature = "discovery-dns-update")]
pub use dns_update::{DnsRegistration, DnsUpdater, TsigAlgo, TsigKey};
#[cfg(feature = "discovery")]
pub use dnssd::DnsSdBackend;
#[cfg(feature = "discovery")]
pub use mdns::MdnsBackend;
#[cfg(feature = "discovery")]
pub use zone::ZoneSnippet;
pub const CA_SERVICE_TYPE: &str = "_epics-ca._tcp";
#[async_trait::async_trait]
pub trait Backend: Send + Sync {
async fn discover(&self) -> Vec<SocketAddr>;
fn subscribe(&self) -> Option<tokio::sync::mpsc::UnboundedReceiver<DiscoveryEvent>> {
None
}
}
#[derive(Debug, Clone)]
pub enum DiscoveryEvent {
Added { instance: String, addr: SocketAddr },
Removed { instance: String, addr: SocketAddr },
}
#[derive(Debug, Clone)]
pub enum DiscoveryConfig {
Static(Vec<SocketAddr>),
#[cfg(feature = "discovery")]
Mdns,
#[cfg(feature = "discovery")]
DnsSd { zone: String },
Composite(Vec<DiscoveryConfig>),
}
pub fn build_backends(cfg: DiscoveryConfig) -> Vec<Box<dyn Backend>> {
match cfg {
DiscoveryConfig::Static(addrs) => vec![Box::new(StaticBackend::new(addrs))],
DiscoveryConfig::Composite(items) => items.into_iter().flat_map(build_backends).collect(),
#[cfg(feature = "discovery")]
DiscoveryConfig::Mdns => match mdns::MdnsBackend::new() {
Ok(b) => vec![Box::new(b)],
Err(e) => {
tracing::warn!(error = %e, "mDNS backend init failed; skipping");
vec![]
}
},
#[cfg(feature = "discovery")]
DiscoveryConfig::DnsSd { zone } => match dnssd::DnsSdBackend::new(zone) {
Ok(b) => vec![Box::new(b)],
Err(e) => {
tracing::warn!(error = %e, "DNS-SD backend init failed; skipping");
vec![]
}
},
}
}
pub fn from_env() -> Option<DiscoveryConfig> {
let raw = epics_base_rs::runtime::env::get("EPICS_CA_DISCOVERY")?;
let mut items: Vec<DiscoveryConfig> = Vec::new();
for token in raw.split_whitespace() {
if let Some(cfg) = parse_token(token) {
items.push(cfg);
}
}
match items.len() {
0 => None,
1 => Some(items.into_iter().next().unwrap()),
_ => Some(DiscoveryConfig::Composite(items)),
}
}
fn parse_token(tok: &str) -> Option<DiscoveryConfig> {
if tok == "mdns" {
#[cfg(feature = "discovery")]
{
return Some(DiscoveryConfig::Mdns);
}
#[cfg(not(feature = "discovery"))]
{
tracing::warn!("EPICS_CA_DISCOVERY=mdns ignored — built without `discovery` feature");
return None;
}
}
if let Some(zone) = tok.strip_prefix("dnssd:") {
#[cfg(feature = "discovery")]
{
return Some(DiscoveryConfig::DnsSd {
zone: zone.to_string(),
});
}
#[cfg(not(feature = "discovery"))]
{
let _ = zone;
tracing::warn!(
"EPICS_CA_DISCOVERY=dnssd:* ignored — built without `discovery` feature"
);
return None;
}
}
if let Some(rest) = tok.strip_prefix("static:") {
let addrs: Vec<SocketAddr> = rest
.split(',')
.filter(|s| !s.is_empty())
.filter_map(parse_static_addr)
.collect();
if addrs.is_empty() {
tracing::warn!(token = %tok, "EPICS_CA_DISCOVERY static: parsed no addresses");
return None;
}
return Some(DiscoveryConfig::Static(addrs));
}
tracing::warn!(token = %tok, "EPICS_CA_DISCOVERY: unrecognized token");
None
}
fn parse_static_addr(entry: &str) -> Option<SocketAddr> {
use std::net::IpAddr;
if let Ok(addr) = entry.parse::<SocketAddr>() {
return Some(addr);
}
if let Ok(ip) = entry.parse::<IpAddr>() {
let port = epics_base_rs::runtime::env::get("EPICS_CA_SERVER_PORT")
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(crate::protocol::CA_SERVER_PORT);
return Some(SocketAddr::new(ip, port));
}
tracing::warn!(entry = %entry, "EPICS_CA_DISCOVERY static: dropped unparseable address");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_static_token() {
let cfg = parse_token("static:10.0.0.1:5064,10.0.0.2:5064").unwrap();
match cfg {
DiscoveryConfig::Static(addrs) => assert_eq!(addrs.len(), 2),
_ => panic!("wrong variant"),
}
}
#[test]
fn parse_unknown_token_is_none() {
assert!(parse_token("foo:bar").is_none());
}
#[test]
fn parse_static_token_defaults_bare_addr_to_ca_port() {
let cfg = parse_token("static:10.0.0.1,10.0.0.2:5066").unwrap();
match cfg {
DiscoveryConfig::Static(addrs) => {
assert_eq!(addrs.len(), 2);
assert_eq!(addrs[0].port(), crate::protocol::CA_SERVER_PORT);
assert_eq!(addrs[1].port(), 5066);
}
_ => panic!("wrong variant"),
}
}
#[cfg(feature = "discovery")]
#[test]
fn parse_mdns_token() {
assert!(matches!(parse_token("mdns"), Some(DiscoveryConfig::Mdns)));
}
#[cfg(feature = "discovery")]
#[test]
fn parse_dnssd_token() {
let cfg = parse_token("dnssd:facility.local").unwrap();
match cfg {
DiscoveryConfig::DnsSd { zone } => assert_eq!(zone, "facility.local"),
_ => panic!("wrong variant"),
}
}
}