use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
use std::time::{Duration, Instant};
pub(super) const REG_DOMAIN_TTL: Duration = Duration::from_secs(3);
pub(super) const REG_DOMAIN_READ_TIMEOUT: Duration = Duration::from_secs(2);
pub(super) const REG_DOMAIN_UNKNOWN: &str = "unknown";
pub(super) struct RegDomainCache {
held: Mutex<Option<(Instant, String)>>,
refreshing: AtomicBool,
}
impl RegDomainCache {
pub(super) const fn new() -> Self {
Self {
held: Mutex::new(None),
refreshing: AtomicBool::new(false),
}
}
pub(super) fn peek_at(&self, now: Instant, ttl: Duration) -> (String, bool) {
match self.held.lock() {
Ok(held) => match held.as_ref() {
Some((at, value)) if now.duration_since(*at) < ttl => (value.clone(), false),
Some((_, value)) => (value.clone(), true),
None => (REG_DOMAIN_UNKNOWN.to_string(), true),
},
Err(_) => (REG_DOMAIN_UNKNOWN.to_string(), true),
}
}
pub(super) fn store_at(&self, now: Instant, value: String) {
if let Ok(mut held) = self.held.lock() {
*held = Some((now, value));
}
}
pub(super) fn claim_refresh(&self) -> bool {
self.refreshing
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
}
pub(super) fn release_refresh(&self) {
self.refreshing.store(false, Ordering::Release);
}
}
static REG_DOMAIN_CACHE: RegDomainCache = RegDomainCache::new();
pub fn regulatory_domain() -> String {
let (value, due) = REG_DOMAIN_CACHE.peek_at(Instant::now(), REG_DOMAIN_TTL);
if due {
schedule_regulatory_refresh();
}
value
}
pub async fn refresh_regulatory_domain() -> String {
let value = read_regulatory_domain().await;
REG_DOMAIN_CACHE.store_at(Instant::now(), value.clone());
value
}
pub(super) fn schedule_regulatory_refresh() {
let Ok(handle) = tokio::runtime::Handle::try_current() else {
return;
};
if !REG_DOMAIN_CACHE.claim_refresh() {
return;
}
handle.spawn(async {
let value = read_regulatory_domain().await;
REG_DOMAIN_CACHE.store_at(Instant::now(), value);
REG_DOMAIN_CACHE.release_refresh();
});
}
pub(super) async fn read_regulatory_domain() -> String {
let reading = tokio::process::Command::new("iw")
.args(["reg", "get"])
.kill_on_drop(true)
.output();
match tokio::time::timeout(REG_DOMAIN_READ_TIMEOUT, reading).await {
Ok(Ok(out)) if out.status.success() => parse_reg_get(&String::from_utf8_lossy(&out.stdout)),
_ => REG_DOMAIN_UNKNOWN.to_string(),
}
}
pub(super) fn parse_reg_get(stdout: &str) -> String {
for line in stdout.lines() {
let stripped = line.trim();
if let Some(rest) = stripped.strip_prefix("country ") {
let code = rest.split(':').next().unwrap_or("").trim();
if code.is_empty() {
return REG_DOMAIN_UNKNOWN.to_string();
}
return code.to_string();
}
if stripped.starts_with("global") {
return "global".to_string();
}
}
REG_DOMAIN_UNKNOWN.to_string()
}