use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use system_configuration::core_foundation::array::CFArray;
use system_configuration::core_foundation::runloop::{CFRunLoop, kCFRunLoopCommonModes};
use system_configuration::core_foundation::string::CFString;
use system_configuration::dynamic_store::{
SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext,
};
use crate::error::{Error, Result};
use crate::ownership::ResourceId;
use crate::watch::{DnsEvent, WatchCallback};
struct WatchState {
sender: mpsc::Sender<ResourceId>,
}
fn sc_callout(_store: SCDynamicStore, changed_keys: CFArray<CFString>, info: &mut WatchState) {
for key in changed_keys.iter() {
let key = key.to_string();
let resource = if let Some(rest) = key.strip_prefix("State:/Network/Service/") {
rest.strip_suffix("/DNS")
.and_then(|id| super::MacosBackend::service_resource(id).ok())
} else if key == "State:/Network/Global/IPv4" {
ResourceId::new("macos:global").ok()
} else {
None
};
if let Some(resource) = resource {
let _ = info.sender.send(resource);
}
}
}
struct RunLoopHandle(CFRunLoop);
unsafe impl Send for RunLoopHandle {}
pub(crate) fn start_store_watch(
flag: Arc<std::sync::atomic::AtomicBool>,
callback: WatchCallback,
) -> Result<Box<dyn FnOnce() + Send>> {
let (tx, rx) = mpsc::channel::<ResourceId>();
let shared: Arc<Mutex<Option<RunLoopHandle>>> = Arc::new(Mutex::new(None));
let (notify_ready, ready) = mpsc::sync_channel::<std::result::Result<(), String>>(1);
let worker_flag = flag.clone();
let worker = thread::Builder::new()
.name("osdns-sc-worker".to_string())
.spawn(move || {
for resource in rx {
if worker_flag.load(Ordering::Acquire) {
break;
}
callback(&DnsEvent::ResourceChanged { resource });
}
})
.map_err(|e| Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: format!("cannot spawn SC worker thread: {e}"),
})?;
let thread_flag = flag.clone();
let shared_for_thread = Arc::clone(&shared);
let watch = thread::Builder::new()
.name("osdns-sc-watch".to_string())
.spawn(move || {
let state = WatchState { sender: tx };
let context = SCDynamicStoreCallBackContext {
callout: sc_callout,
info: state,
};
let Some(store) = SCDynamicStoreBuilder::new("osdns-watch")
.callback_context(context)
.build()
else {
let _ = notify_ready.send(Err(
"cannot open the SystemConfiguration dynamic store".to_string()
));
return;
};
let patterns = CFArray::from_CFTypes(&[
CFString::new("State:/Network/Service/.*[/]DNS"),
CFString::new("State:/Network/Global/IPv4"),
]);
let keys: CFArray<CFString> = CFArray::from_CFTypes(&[]);
if !store.set_notification_keys(&keys, &patterns) {
let _ = notify_ready.send(Err(
"cannot subscribe to SystemConfiguration DNS notifications".to_string(),
));
return;
}
let Some(source) = store.create_run_loop_source() else {
let _ = notify_ready.send(Err(
"cannot create the SystemConfiguration run-loop source".to_string(),
));
return;
};
let run_loop = CFRunLoop::get_current();
run_loop.add_source(&source, unsafe { kCFRunLoopCommonModes });
*shared_for_thread
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(RunLoopHandle(run_loop));
let _ = notify_ready.send(Ok(()));
if thread_flag.load(Ordering::Acquire) {
return;
}
CFRunLoop::run_current();
})
.map_err(|e| Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: format!("cannot spawn SC watch thread: {e}"),
})?;
match ready.recv() {
Ok(Ok(())) => {}
Ok(Err(message)) => {
flag.store(true, Ordering::Release);
let _ = watch.join();
let _ = worker.join();
return Err(Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message,
});
}
Err(_) => {
flag.store(true, Ordering::Release);
let _ = watch.join();
let _ = worker.join();
return Err(Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: "SystemConfiguration watch initialization terminated unexpectedly"
.to_string(),
});
}
}
let cancel_shared = Arc::clone(&shared);
Ok(Box::new(move || {
flag.store(true, Ordering::Release);
let handle = cancel_shared
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.take();
if let Some(handle) = handle {
handle.0.stop();
}
let _ = watch.join();
let _ = worker.join();
}))
}
const RESOLVER_DIR: &str = "/etc/resolver";
fn resolver_resource_from_path(path: &std::path::Path) -> Option<ResourceId> {
let parent = path.parent()?.to_string_lossy().to_string();
if parent != RESOLVER_DIR && parent != "/private/etc/resolver" {
return None;
}
let domain = path.file_name()?.to_string_lossy().to_string();
if domain.is_empty() || domain.starts_with('.') {
return None;
}
ResourceId::new(format!("macos:resolver:{domain}")).ok()
}
pub(crate) fn start_resolver_watch(
flag: Arc<std::sync::atomic::AtomicBool>,
callback: WatchCallback,
) -> Result<Box<dyn FnOnce() + Send>> {
use notify::Watcher;
let (tx, rx) = mpsc::channel::<DnsEvent>();
let handler_flag = flag.clone();
let mut watcher = notify::recommended_watcher(
move |event: std::result::Result<notify::Event, notify::Error>| {
if handler_flag.load(Ordering::Acquire) {
return;
}
let Ok(event) = event else { return };
for path in event.paths {
let Some(resource) = resolver_resource_from_path(&path) else {
continue;
};
let removed = matches!(event.kind, notify::event::EventKind::Remove(_));
let event = if removed {
DnsEvent::ResourceRemoved { resource }
} else {
DnsEvent::ResourceChanged { resource }
};
let _ = tx.send(event);
}
},
)
.map_err(|e| Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: format!("cannot create resolver file watcher: {e}"),
})?;
let parent = std::fs::canonicalize("/etc")?;
watcher
.watch(&parent, notify::RecursiveMode::Recursive)
.map_err(|e| Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: format!(
"cannot watch {} for resolver changes: {e}",
parent.display()
),
})?;
let worker_flag = flag.clone();
let worker = thread::Builder::new()
.name("osdns-resolver-worker".to_string())
.spawn(move || {
for event in rx {
if worker_flag.load(Ordering::Acquire) {
break;
}
callback(&event);
}
})
.map_err(|e| Error::Platform {
backend: crate::capability::BackendKind::MacosSystemConfiguration,
message: format!("cannot spawn resolver worker thread: {e}"),
})?;
Ok(Box::new(move || {
flag.store(true, Ordering::Release);
drop(watcher);
let _ = worker.join();
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolver_paths_map_to_resources() {
for path in [
"/etc/resolver/corp.example",
"/private/etc/resolver/corp.example",
] {
let resource = resolver_resource_from_path(std::path::Path::new(path)).unwrap();
assert_eq!(resource.as_str(), "macos:resolver:corp.example");
}
}
#[test]
fn non_resolver_paths_do_not_map() {
assert!(resolver_resource_from_path(std::path::Path::new("/etc/hosts")).is_none());
assert!(
resolver_resource_from_path(std::path::Path::new("/etc/resolver/.hidden")).is_none()
);
}
#[test]
fn nested_and_unrelated_canonical_paths_do_not_map() {
for path in [
"/private/etc/hosts",
"/private/etc/resolver",
"/private/etc/resolver/subdir/corp.example",
] {
assert!(resolver_resource_from_path(std::path::Path::new(path)).is_none());
}
}
}