use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use windows::Win32::Foundation::{ERROR_SUCCESS, HANDLE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_NOTIFY, REG_NOTIFY_CHANGE_LAST_SET,
REG_NOTIFY_CHANGE_NAME, REG_NOTIFY_FILTER, REG_NOTIFY_THREAD_AGNOSTIC, RegNotifyChangeKeyValue,
};
use windows::Win32::System::Threading::{INFINITE, SetEvent, WaitForMultipleObjects};
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::ffi::{Event, RegKey, SendPtr, hresult_error, win32_error};
use super::{INTERNET_SETTINGS, POLICY_INTERNET_SETTINGS, read_config};
const NOTIFY_FILTER: REG_NOTIFY_FILTER = REG_NOTIFY_FILTER(
REG_NOTIFY_CHANGE_LAST_SET.0 | REG_NOTIFY_CHANGE_NAME.0 | REG_NOTIFY_THREAD_AGNOSTIC.0,
);
const OPENING_USER_KEY: &str = "opening HKCU Internet Settings key for notifications";
#[derive(Debug)]
pub(crate) struct Watch {
shutdown: Event,
poll_now: Event,
watched: Option<Vec<WatchedKey>>,
construction_degraded: Vec<ProxyConfigSource>,
live_at_construction: usize,
thread: Option<JoinHandle<()>>,
}
impl Watch {
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
pub(crate) fn armed(options: &WatchOptions) -> Result<Self, Error> {
let shutdown = Event::new(true, "creating the proxy-watch shutdown event")?;
let poll_now = Event::new(false, "creating the proxy-watch poll_now event")?;
let mut watched = Vec::with_capacity(3);
let mut construction_degraded = Vec::new();
let (user_key, leading) = match WatchedKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
true,
ProxyConfigSource::Registry,
OPENING_USER_KEY,
) {
Ok(Some(key)) => (key.arm().map(|()| key), true),
Ok(None) => (
Err(Error::io(
OPENING_USER_KEY,
std::io::Error::from(std::io::ErrorKind::NotFound),
)),
false,
),
Err(error) => (Err(error), true),
};
match watch_fail_soft(leading, options.poll_interval, user_key) {
WatchFailSoft::Live(key) => watched.push(key),
WatchFailSoft::Degraded(error) => {
crate::trace::warning!(
key = INTERNET_SETTINGS,
error = %crate::trace::SafeError(&error),
"there is no HKCU Internet Settings notification route, so the watcher \
continues without one; WatchOptions::poll_interval still re-reads on a \
timer if it is set"
);
push_degraded(&mut construction_degraded, ProxyConfigSource::Registry);
}
WatchFailSoft::Fatal(error) => {
return Err(fatal_watch_error(
"opening or arming HKCU Internet Settings for notifications",
"HKCU is Windows' only mandatory proxy configuration source, with no \
fallback route this backend can use instead",
error,
));
}
}
if options.watch_group_policy && !arm_group_policy_key(&mut watched) {
push_degraded(&mut construction_degraded, ProxyConfigSource::GroupPolicy);
}
if !arm_machine_default_key(&mut watched) {
push_degraded(
&mut construction_degraded,
ProxyConfigSource::WinHttpDefault,
);
}
let live_at_construction = watched.len();
crate::trace::debug!(
keys = watched.len(),
user_key = INTERNET_SETTINGS,
group_policy = options.watch_group_policy,
"armed RegNotifyChangeKeyValue over the watched registry subtrees"
);
Ok(Self {
shutdown,
poll_now,
watched: Some(watched),
construction_degraded,
live_at_construction,
thread: None,
})
}
pub(crate) fn health(&self) -> BackendHealth {
BackendHealth {
degraded: self.construction_degraded.clone(),
has_live_notifications: self.live_at_construction > 0,
}
}
pub(crate) fn spawn(
&mut self,
options: &WatchOptions,
shared: Arc<Shared>,
) -> Result<(), Error> {
let watched = self
.watched
.take()
.expect("Watch::spawn must be called exactly once");
let shutdown = SendPtr(self.shutdown.raw().0);
let poll_now = SendPtr(self.poll_now.raw().0);
let options = options.clone();
let thread = thread::Builder::new()
.name("proxy-watch-registry".to_owned())
.spawn(move || {
let _guard = crate::watch::ThreadGuard::new(Arc::clone(&shared));
crate::trace::debug!("the registry watcher thread started");
if let Err(error) = run(shutdown, poll_now, watched, &options, &shared) {
crate::trace::error!(
error = %crate::trace::SafeError(&error),
"the registry watcher thread cannot continue"
);
shared.fail(error);
}
})
.map_err(|source| Error::io("spawning the proxy-watch registry thread", source))?;
self.thread = Some(thread);
Ok(())
}
pub(crate) fn poll_now(&self) {
unsafe {
let _ = SetEvent(self.poll_now.raw());
}
}
}
impl Drop for Watch {
fn drop(&mut self) {
crate::trace::debug!("stopping the registry watcher");
unsafe {
let _ = SetEvent(self.shutdown.raw());
}
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn push_degraded(degraded: &mut Vec<ProxyConfigSource>, source: ProxyConfigSource) {
if !degraded.contains(&source) {
degraded.push(source);
}
}
#[derive(Debug)]
struct WatchedKey {
key: RegKey,
event: Event,
critical: bool,
source: ProxyConfigSource,
#[cfg_attr(not(feature = "tracing"), allow(dead_code))]
path: &'static str,
}
impl WatchedKey {
fn open(
root: HKEY,
path: &'static str,
critical: bool,
source: ProxyConfigSource,
context: &str,
) -> Result<Option<Self>, Error> {
let Some(key) = RegKey::open(root, path, KEY_NOTIFY, context)? else {
return Ok(None);
};
let event = Event::new(false, "creating a registry notification event")?;
Ok(Some(Self {
key,
event,
critical,
source,
path,
}))
}
fn arm(&self) -> Result<(), Error> {
let status = unsafe {
RegNotifyChangeKeyValue(
self.key.raw(),
true,
NOTIFY_FILTER,
Some(self.event.raw()),
true,
)
};
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(win32_error(
"registering a registry change notification",
status,
))
}
}
}
fn group_policy_ancestors() -> Vec<&'static str> {
let mut candidates = Vec::new();
let mut rest = POLICY_INTERNET_SETTINGS;
while let Some(index) = rest.rfind('\\') {
rest = &rest[..index];
candidates.push(rest);
}
candidates
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn arm_first_available(
watched: &mut Vec<WatchedKey>,
root: HKEY,
candidates: &[&'static str],
source: ProxyConfigSource,
what: &str,
) -> bool {
for &path in candidates {
let context = format!("opening {what} ({path}) for notifications");
let key = match WatchedKey::open(root, path, false, source, &context) {
Ok(Some(key)) => key,
Ok(None) => {
crate::trace::warning!(
key = path,
what,
"a secondary registry key does not exist; trying the next candidate"
);
continue;
}
Err(error) => {
crate::trace::warning!(
key = path,
what,
error = %crate::trace::SafeError(&error),
"opening a secondary registry key failed; trying the next candidate"
);
continue;
}
};
if let Err(error) = key.arm() {
crate::trace::warning!(
key = path,
what,
error = %crate::trace::SafeError(&error),
"arming a secondary registry key failed; trying the next candidate"
);
continue;
}
crate::trace::debug!(key = path, what, "armed a secondary registry key");
watched.push(key);
return true;
}
false
}
fn arm_group_policy_key(watched: &mut Vec<WatchedKey>) -> bool {
arm_first_available(
watched,
HKEY_LOCAL_MACHINE,
&group_policy_ancestors(),
ProxyConfigSource::GroupPolicy,
"an HKLM group policy ancestor key",
)
}
fn arm_machine_default_key(watched: &mut Vec<WatchedKey>) -> bool {
arm_first_available(
watched,
HKEY_LOCAL_MACHINE,
&[INTERNET_SETTINGS],
ProxyConfigSource::WinHttpDefault,
"the non-policy HKLM Internet Settings key",
)
}
enum Wake {
Shutdown,
PollNow,
Key(usize),
Timeout,
}
fn poll_wait_millis(poll_interval: Option<Duration>) -> u32 {
match poll_interval {
None => INFINITE,
Some(interval) => u32::try_from(effective_poll_interval(interval).as_millis())
.unwrap_or(u32::MAX)
.min(u32::MAX - 1),
}
}
fn debounce_wait_millis(remaining: Duration) -> u32 {
u32::try_from(remaining.as_millis())
.unwrap_or(u32::MAX)
.min(u32::MAX - 1)
}
fn run(
shutdown: SendPtr,
poll_now: SendPtr,
watched: Vec<WatchedKey>,
options: &WatchOptions,
shared: &Shared,
) -> Result<(), Error> {
let mut handles = Vec::with_capacity(watched.len() + 2);
handles.push(shutdown.handle());
handles.push(poll_now.handle());
handles.extend(watched.iter().map(|w| w.event.raw()));
let poll_millis = poll_wait_millis(options.poll_interval);
let mut alive = vec![true; watched.len()];
loop {
let mut fatal = None;
match wait(&handles, poll_millis)? {
Wake::Shutdown => return Ok(()),
Wake::PollNow => {
crate::trace::debug!("poll_now() opened the debounce window");
}
Wake::Key(index) => {
if let Err(error) =
rearm(&watched, &mut alive, index, options.poll_interval, shared)
{
fatal.get_or_insert(error);
}
crate::trace::debug!(
key = watched[index].path,
"a registry change notification opened the debounce window"
);
}
Wake::Timeout => {
crate::trace::debug!("the poll interval elapsed; opening 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;
}
match wait(&handles, debounce_wait_millis(remaining))? {
Wake::Shutdown => return Ok(()),
Wake::PollNow => {}
Wake::Key(index) => {
if let Err(error) =
rearm(&watched, &mut alive, index, options.poll_interval, shared)
{
fatal.get_or_insert(error);
}
}
Wake::Timeout => break,
}
}
crate::trace::debug!("the debounce window closed; re-reading the registry");
match read_config(options) {
Ok(config) => {
shared.emit(config);
}
Err(error) => shared.fail(error),
}
if let Some(error) = fatal {
return Err(error);
}
}
}
fn mark_degraded_and_check_all_dead(alive: &mut [bool], index: usize) -> bool {
alive[index] = false;
alive.iter().all(|&is_alive| !is_alive)
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn rearm(
watched: &[WatchedKey],
alive: &mut [bool],
index: usize,
poll_interval: Option<Duration>,
shared: &Shared,
) -> Result<(), Error> {
let key = &watched[index];
match watch_fail_soft(key.critical, poll_interval, key.arm()) {
WatchFailSoft::Live(()) => Ok(()),
WatchFailSoft::Degraded(error) => {
crate::trace::warning!(
key = key.path,
error = %crate::trace::SafeError(&error),
"re-arming a registry watch failed; it will no longer notify for this key"
);
shared.degrade(key.source);
if mark_degraded_and_check_all_dead(alive, index) {
shared.mark_no_live_notifications();
}
Ok(())
}
WatchFailSoft::Fatal(error) => Err(fatal_watch_error(
"re-arming HKCU Internet Settings for notifications",
"HKCU is Windows' only mandatory proxy configuration source, with no fallback \
route this backend can use instead",
error,
)),
}
}
fn wait(handles: &[HANDLE], millis: u32) -> Result<Wake, Error> {
let result = unsafe { WaitForMultipleObjects(handles, false, millis) };
if result == WAIT_TIMEOUT {
return Ok(Wake::Timeout);
}
if result == WAIT_FAILED {
return Err(hresult_error(
"waiting for registry change notifications",
windows::core::Error::from_thread(),
));
}
let index = result.0.wrapping_sub(WAIT_OBJECT_0.0) as usize;
match index {
0 => Ok(Wake::Shutdown),
1 => Ok(Wake::PollNow),
i if i < handles.len() => Ok(Wake::Key(i - 2)),
_ => Err(Error::io(
"waiting for registry change notifications",
std::io::Error::other(format!("unexpected wait result {result:?}")),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_poll_interval_waits_forever() {
assert_eq!(poll_wait_millis(None), INFINITE);
}
#[test]
fn a_poll_interval_converts_to_milliseconds() {
assert_eq!(poll_wait_millis(Some(Duration::from_secs(30))), 30_000);
}
#[test]
fn a_too_small_poll_interval_is_clamped_before_converting() {
let floor_millis = u32::try_from(crate::watch::MIN_POLL_INTERVAL.as_millis())
.expect("the floor fits u32 milliseconds");
assert_eq!(
poll_wait_millis(Some(Duration::from_millis(1))),
floor_millis
);
assert_eq!(poll_wait_millis(Some(Duration::ZERO)), floor_millis);
}
#[test]
fn an_overlong_poll_interval_is_clamped_below_infinite() {
let millis = poll_wait_millis(Some(Duration::MAX));
assert_eq!(millis, u32::MAX - 1);
assert!(millis < INFINITE);
let millis = poll_wait_millis(Some(Duration::from_millis(u64::from(u32::MAX))));
assert_eq!(millis, u32::MAX - 1);
assert!(millis < INFINITE);
}
#[test]
fn an_overlong_debounce_window_is_clamped_below_infinite() {
for remaining in [
Duration::MAX,
Duration::from_millis(u64::from(u32::MAX)),
Duration::from_millis(u64::from(u32::MAX) + 1),
] {
let millis = debounce_wait_millis(remaining);
assert_eq!(millis, u32::MAX - 1, "{remaining:?}");
assert!(millis < INFINITE, "{remaining:?}");
}
}
#[test]
fn an_ordinary_debounce_window_converts_to_milliseconds() {
assert_eq!(debounce_wait_millis(Duration::from_millis(250)), 250);
assert_eq!(debounce_wait_millis(Duration::ZERO), 0);
assert_eq!(
debounce_wait_millis(crate::watch::WatchOptions::default().debounce),
u32::try_from(crate::watch::WatchOptions::default().debounce.as_millis())
.expect("the default debounce fits u32 milliseconds")
);
}
#[test]
fn the_leaf_key_itself_is_never_a_candidate() {
assert!(
!group_policy_ancestors().contains(&POLICY_INTERNET_SETTINGS),
"the leaf key is back in the candidate list"
);
}
#[test]
fn ancestors_go_from_the_specific_leaf_to_the_broad_root() {
let candidates = group_policy_ancestors();
assert_eq!(
candidates.first().copied(),
POLICY_INTERNET_SETTINGS
.rsplit_once('\\')
.map(|(head, _)| head),
"the most specific candidate is the leaf's immediate parent"
);
assert_eq!(candidates.last(), Some(&"Software"));
assert!(
candidates
.iter()
.all(|candidate| POLICY_INTERNET_SETTINGS.starts_with(candidate)),
"every candidate must be an ancestor path of the leaf"
);
for pair in candidates.windows(2) {
assert!(
pair[0].len() > pair[1].len(),
"{:?} is not more specific than {:?}",
pair[0],
pair[1]
);
assert!(
pair[0].starts_with(pair[1]),
"{:?} must be an ancestor path of {:?}",
pair[1],
pair[0]
);
}
}
#[test]
fn push_degraded_does_not_duplicate_a_source() {
let mut degraded = Vec::new();
push_degraded(&mut degraded, ProxyConfigSource::Registry);
push_degraded(&mut degraded, ProxyConfigSource::Registry);
assert_eq!(degraded, vec![ProxyConfigSource::Registry]);
push_degraded(&mut degraded, ProxyConfigSource::GroupPolicy);
assert_eq!(
degraded,
vec![ProxyConfigSource::Registry, ProxyConfigSource::GroupPolicy]
);
}
#[test]
fn only_the_last_alive_key_dying_reports_total_loss() {
let mut alive = vec![true, true, true];
assert!(!mark_degraded_and_check_all_dead(&mut alive, 0));
assert_eq!(alive, vec![false, true, true]);
assert!(!mark_degraded_and_check_all_dead(&mut alive, 1));
assert_eq!(alive, vec![false, false, true]);
assert!(mark_degraded_and_check_all_dead(&mut alive, 2));
assert_eq!(alive, vec![false, false, false]);
}
#[test]
fn a_single_key_dying_is_immediately_total_loss() {
let mut alive = vec![true];
assert!(mark_degraded_and_check_all_dead(&mut alive, 0));
}
#[test]
fn marking_an_already_dead_key_dead_again_is_a_harmless_no_op() {
let mut alive = vec![false, true];
assert!(!mark_degraded_and_check_all_dead(&mut alive, 0));
assert_eq!(alive, vec![false, true]);
}
#[test]
fn a_signalled_handle_names_the_watched_key_that_owns_it() {
use windows::Win32::System::Threading::ResetEvent;
let events: Vec<Event> = (0..5)
.map(|_| Event::new(true, "creating a wait-mapping test event").expect("event"))
.collect();
let handles: Vec<HANDLE> = events.iter().map(|event| event.raw()).collect();
assert!(
matches!(wait(&handles, 0), Ok(Wake::Timeout)),
"with nothing signalled a wait must name no handle at all"
);
for (index, event) in events.iter().enumerate() {
unsafe { SetEvent(event.raw()) }.expect("signalling the test event");
match wait(&handles, 0) {
Ok(Wake::Shutdown) => assert_eq!(index, 0, "handle 0 is the shutdown event"),
Ok(Wake::PollNow) => assert_eq!(index, 1, "handle 1 is the poll_now event"),
Ok(Wake::Key(key)) => assert_eq!(
key + 2,
index,
"handle {index} belongs to watched[{}], not watched[{key}]",
index - 2
),
Ok(Wake::Timeout) => panic!("handle {index} is signalled, so nothing timed out"),
Err(error) => panic!("handle {index}: {error}"),
}
unsafe { ResetEvent(event.raw()) }.expect("clearing the test event");
}
}
#[test]
fn turning_group_policy_off_arms_one_fewer_key() {
let watched_with = Watch::armed(&WatchOptions::new()).expect("arming with group policy");
let watched_without = Watch::armed(&WatchOptions::new().with_group_policy(false))
.expect("arming without group policy");
assert!(
!watched_without
.construction_degraded
.contains(&ProxyConfigSource::GroupPolicy),
"opting out is not degradation: {:?}",
watched_without.construction_degraded
);
assert_eq!(
watched_with.live_at_construction,
watched_without.live_at_construction + 1,
"group policy watching must add exactly the one route it names"
);
}
}