use std::ffi::c_void;
use std::io;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use core_foundation::array::CFArray;
use core_foundation::base::{TCFType, kCFAllocatorDefault};
use core_foundation::runloop::{
CFRunLoop, CFRunLoopRunResult, CFRunLoopSource, CFRunLoopSourceContext, CFRunLoopSourceCreate,
CFRunLoopSourceSignal, CFRunLoopWakeUp, kCFRunLoopDefaultMode,
};
use core_foundation::string::CFString;
use system_configuration::dynamic_store::{
SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext,
};
use crate::config::ProxyConfigSource;
use crate::error::Error;
use crate::watch::{
BackendHealth, Shared, WatchFailSoft, WatchOptions, effective_debounce,
effective_poll_interval, fatal_watch_error, watch_fail_soft,
};
use super::{SETUP_PROXIES_KEY, STATE_PROXIES_KEY, STORE_NAME, read_config};
const IDLE_WAIT: Duration = Duration::from_secs(60 * 60);
fn idle_wait(poll_interval: Option<Duration>) -> Duration {
match poll_interval {
Some(interval) => effective_poll_interval(interval).min(IDLE_WAIT),
None => IDLE_WAIT,
}
}
pub(super) const REGISTRATION_RETRY_INTERVAL: Duration = Duration::from_secs(1);
pub(super) const REGISTRATION_MAX_RETRIES: u32 = 5;
pub(super) fn should_retry(attempt: u32) -> bool {
attempt < REGISTRATION_MAX_RETRIES
}
struct WakeSource(CFRunLoopSource);
unsafe impl Send for WakeSource {}
impl Clone for WakeSource {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl WakeSource {
fn new() -> Result<Self, Error> {
extern "C" fn perform(_info: *const c_void) {}
let mut context = CFRunLoopSourceContext {
version: 0,
info: ptr::null_mut(),
retain: None,
release: None,
copyDescription: None,
equal: None,
hash: None,
schedule: None,
cancel: None,
perform,
};
let raw = unsafe { CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &raw mut context) };
if raw.is_null() {
return Err(Error::io(
"creating the proxy-watch wake-up run loop source",
io::Error::other("CFRunLoopSourceCreate returned NULL"),
));
}
Ok(Self(unsafe {
CFRunLoopSource::wrap_under_create_rule(raw)
}))
}
fn signal(&self, run_loop: Option<&CFRunLoop>) {
unsafe { CFRunLoopSourceSignal(self.0.as_concrete_TypeRef()) };
if let Some(run_loop) = run_loop {
unsafe { CFRunLoopWakeUp(run_loop.as_concrete_TypeRef()) };
}
}
}
pub(crate) struct Watch {
stop: Arc<AtomicBool>,
wake: WakeSource,
poll_requested: Arc<AtomicBool>,
run_loop: Option<CFRunLoop>,
degraded: bool,
thread: Option<JoinHandle<()>>,
}
impl std::fmt::Debug for Watch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Watch")
.field("stopped", &self.stop.load(Ordering::SeqCst))
.field("running", &self.thread.is_some())
.field("degraded", &self.degraded)
.finish_non_exhaustive()
}
}
impl Watch {
pub(crate) fn armed(_options: &WatchOptions) -> Result<Self, Error> {
Ok(Self {
stop: Arc::new(AtomicBool::new(false)),
wake: WakeSource::new()?,
poll_requested: Arc::new(AtomicBool::new(false)),
run_loop: None,
degraded: false,
thread: None,
})
}
pub(crate) fn spawn(
&mut self,
options: &WatchOptions,
shared: Arc<Shared>,
) -> Result<(), Error> {
assert!(
self.thread.is_none(),
"Watch::spawn must be called exactly once"
);
let wake = self.wake.clone();
let stop = Arc::clone(&self.stop);
let poll_requested = Arc::clone(&self.poll_requested);
let options = options.clone();
let (ready_tx, ready_rx) = mpsc::sync_channel::<Result<(CFRunLoop, bool), Error>>(1);
let thread = thread::Builder::new()
.name("proxy-watch-scdynamicstore".to_owned())
.spawn(move || {
let _guard = crate::watch::ThreadGuard::new(Arc::clone(&shared));
crate::trace::debug!("the SCDynamicStore watcher thread started");
let changed = Arc::new(AtomicBool::new(false));
match Registration::new(&wake, Arc::clone(&changed), options.poll_interval) {
Ok(registration) => {
let _ = ready_tx
.send(Ok((registration.run_loop.clone(), registration.degraded)));
drop(ready_tx);
run(&options, &shared, &stop, &changed, &poll_requested);
drop(registration);
}
Err(error) => {
let _ = ready_tx.send(Err(error));
}
}
})
.map_err(|source| {
Error::io("spawning the proxy-watch SCDynamicStore thread", source)
})?;
self.thread = Some(thread);
match ready_rx.recv() {
Ok(Ok((run_loop, degraded))) => {
self.run_loop = Some(run_loop);
self.degraded = degraded;
Ok(())
}
Ok(Err(error)) => Err(error),
Err(_) => Err(Error::io(
"starting the proxy-watch SCDynamicStore thread",
io::Error::other("the watcher thread exited before it became ready"),
)),
}
}
pub(crate) fn health(&self) -> BackendHealth {
if self.degraded {
BackendHealth {
degraded: vec![
ProxyConfigSource::SystemConfigurationSetup,
ProxyConfigSource::SystemConfigurationState,
],
has_live_notifications: false,
}
} else {
BackendHealth {
degraded: Vec::new(),
has_live_notifications: true,
}
}
}
pub(crate) fn poll_now(&self) {
self.poll_requested.store(true, Ordering::SeqCst);
self.wake.signal(self.run_loop.as_ref());
}
}
impl Drop for Watch {
fn drop(&mut self) {
crate::trace::debug!("stopping the SCDynamicStore watcher");
self.stop.store(true, Ordering::SeqCst);
self.wake.signal(self.run_loop.as_ref());
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
enum RegistrationFailure {
Retryable(Error),
Fatal(Error),
}
struct Registration {
_store: Option<SCDynamicStore>,
_source: Option<CFRunLoopSource>,
run_loop: CFRunLoop,
degraded: bool,
}
impl Registration {
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn new(
wake: &WakeSource,
changed: Arc<AtomicBool>,
poll_interval: Option<Duration>,
) -> Result<Self, Error> {
let run_loop = CFRunLoop::get_current();
let mode = unsafe { kCFRunLoopDefaultMode };
run_loop.add_source(&wake.0, mode);
match watch_fail_soft(
true,
poll_interval,
Self::establish_store(&run_loop, changed),
) {
WatchFailSoft::Live((store, source)) => Ok(Self {
_store: Some(store),
_source: Some(source),
run_loop,
degraded: false,
}),
WatchFailSoft::Degraded(error) => {
crate::trace::warning!(
setup_key = SETUP_PROXIES_KEY,
state_key = STATE_PROXIES_KEY,
error = %crate::trace::SafeError(&error),
"registering the SCDynamicStore proxies notification failed and will \
not be retried; continuing on WatchOptions::poll_interval alone"
);
Ok(Self {
_store: None,
_source: None,
run_loop,
degraded: true,
})
}
WatchFailSoft::Fatal(error) => Err(fatal_watch_error(
"registering the SCDynamicStore proxies notification",
"SCDynamicStore is macOS' only proxy change-notification route, with no \
other source this backend can fall back to",
error,
)),
}
}
fn establish_store(
run_loop: &CFRunLoop,
changed: Arc<AtomicBool>,
) -> Result<(SCDynamicStore, CFRunLoopSource), Error> {
let mut attempt = 0u32;
loop {
match Self::try_register_once(run_loop, Arc::clone(&changed)) {
Ok(established) => return Ok(established),
Err(RegistrationFailure::Fatal(error)) => return Err(error),
Err(RegistrationFailure::Retryable(error)) => {
if !should_retry(attempt) {
return Err(error);
}
attempt += 1;
crate::trace::warning!(
attempt,
max_retries = REGISTRATION_MAX_RETRIES,
error = %crate::trace::SafeError(&error),
"registering the SCDynamicStore proxies notification failed; \
retrying after a delay (Chromium's kRetryInterval/kMaxRetry, \
network_config_watcher_apple.cc)"
);
thread::sleep(REGISTRATION_RETRY_INTERVAL);
}
}
}
}
fn try_register_once(
run_loop: &CFRunLoop,
changed: Arc<AtomicBool>,
) -> Result<(SCDynamicStore, CFRunLoopSource), RegistrationFailure> {
let store = SCDynamicStoreBuilder::new(STORE_NAME)
.callback_context(SCDynamicStoreCallBackContext {
callout: on_change,
info: changed,
})
.build()
.ok_or_else(|| {
RegistrationFailure::Retryable(Error::io(
"creating the watching SCDynamicStore session",
io::Error::other("SCDynamicStoreCreateWithOptions returned NULL"),
))
})?;
let keys = CFArray::from_CFTypes(&[
CFString::from_static_string(SETUP_PROXIES_KEY),
CFString::from_static_string(STATE_PROXIES_KEY),
]);
let patterns: CFArray<CFString> = CFArray::from_CFTypes(&[]);
if !store.set_notification_keys(&keys, &patterns) {
return Err(RegistrationFailure::Retryable(Error::io(
"subscribing to Setup:/State: Network/Global/Proxies",
io::Error::other("SCDynamicStoreSetNotificationKeys failed"),
)));
}
crate::trace::debug!(
setup_key = SETUP_PROXIES_KEY,
state_key = STATE_PROXIES_KEY,
"subscribed to the SCDynamicStore proxies keys"
);
let source = store.create_run_loop_source().ok_or_else(|| {
RegistrationFailure::Fatal(Error::io(
"creating the SCDynamicStore run loop source",
io::Error::other("SCDynamicStoreCreateRunLoopSource returned NULL"),
))
})?;
let mode = unsafe { kCFRunLoopDefaultMode };
run_loop.add_source(&source, mode);
Ok((store, source))
}
}
fn on_change(
_store: SCDynamicStore,
_changed_keys: CFArray<CFString>,
changed: &mut Arc<AtomicBool>,
) {
changed.store(true, Ordering::SeqCst);
}
fn run(
options: &WatchOptions,
shared: &Shared,
stop: &AtomicBool,
changed: &AtomicBool,
poll_requested: &AtomicBool,
) {
publish(options, shared);
let mode = unsafe { kCFRunLoopDefaultMode };
let wait = idle_wait(options.poll_interval);
while !stop.load(Ordering::SeqCst) {
let result = CFRunLoop::run_in_mode(mode, wait, true);
if stop.load(Ordering::SeqCst) {
return;
}
let polled = options.poll_interval.is_some() && result == CFRunLoopRunResult::TimedOut;
let requested = poll_requested.load(Ordering::SeqCst);
if !changed.load(Ordering::SeqCst) && !polled && !requested {
continue;
}
if polled {
crate::trace::debug!("the poll interval elapsed; opening the debounce window");
}
if requested {
crate::trace::debug!("poll_now() opened the debounce window");
}
let deadline = Instant::now() + effective_debounce(options.debounce);
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
CFRunLoop::run_in_mode(mode, remaining, true);
if stop.load(Ordering::SeqCst) {
return;
}
}
changed.store(false, Ordering::SeqCst);
poll_requested.store(false, Ordering::SeqCst);
crate::trace::debug!("the debounce window closed; re-reading the dynamic store");
publish(options, shared);
}
}
fn publish(options: &WatchOptions, shared: &Shared) {
match read_config(options) {
Ok(config) => shared.emit(config),
Err(error) => shared.fail(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_poll_interval_waits_the_full_idle_bound() {
assert_eq!(idle_wait(None), IDLE_WAIT);
}
#[test]
fn a_shorter_poll_interval_wins() {
assert_eq!(
idle_wait(Some(Duration::from_secs(30))),
Duration::from_secs(30)
);
}
#[test]
fn a_too_small_poll_interval_is_clamped_to_the_floor() {
assert_eq!(
idle_wait(Some(Duration::ZERO)),
crate::watch::MIN_POLL_INTERVAL
);
assert_eq!(
idle_wait(Some(Duration::from_millis(1))),
crate::watch::MIN_POLL_INTERVAL
);
}
#[test]
fn a_poll_interval_longer_than_the_idle_bound_is_capped() {
assert_eq!(idle_wait(Some(IDLE_WAIT * 2)), IDLE_WAIT);
assert_eq!(idle_wait(Some(Duration::MAX)), IDLE_WAIT);
}
#[test]
fn retries_stop_exactly_at_the_chromium_derived_maximum() {
for attempt in 0..REGISTRATION_MAX_RETRIES {
assert!(
should_retry(attempt),
"attempt {attempt} should still retry"
);
}
assert!(!should_retry(REGISTRATION_MAX_RETRIES));
assert!(!should_retry(REGISTRATION_MAX_RETRIES + 1));
}
#[test]
fn the_documented_worst_case_retry_delay_is_five_seconds() {
assert_eq!(
REGISTRATION_RETRY_INTERVAL * REGISTRATION_MAX_RETRIES,
Duration::from_secs(5)
);
}
}