use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, SyncSender};
use std::thread::{self, JoinHandle};
use crate::config::ProxyConfigSource;
use crate::error::Error;
use super::desktop::Reading;
use super::gio_dl::{self, MainContext, Schema, SchemaSource, Settings};
use super::gsettings_map::{
self, CHILDREN, GValue, GnomeSettings, KEY_AUTHENTICATION_PASSWORD, KEY_AUTHENTICATION_USER,
KEY_AUTOCONFIG_URL, KEY_HOST, KEY_IGNORE_HOSTS, KEY_MODE, KEY_PORT, KEY_USE_AUTHENTICATION,
KEY_USE_SAME_PROXY, SCHEMA,
};
pub(crate) const READ_AUTHENTICATION_PASSWORD: bool = false;
#[derive(Debug, Clone, Copy)]
enum Kind {
Text,
Flag,
Int,
List,
}
const ROOT_KEYS: [(&str, Kind); 4] = [
(KEY_MODE, Kind::Text),
(KEY_AUTOCONFIG_URL, Kind::Text),
(KEY_IGNORE_HOSTS, Kind::List),
(KEY_USE_SAME_PROXY, Kind::Flag),
];
const CHILD_KEYS: [(&str, Kind); 2] = [(KEY_HOST, Kind::Text), (KEY_PORT, Kind::Int)];
const HTTP_AUTH_KEYS: [(&str, Kind); 2] = [
(KEY_USE_AUTHENTICATION, Kind::Flag),
(KEY_AUTHENTICATION_USER, Kind::Text),
];
pub(crate) fn read_store() -> Result<Reading, Error> {
let Some(settings) = read_settings() else {
crate::trace::debug!(
schema = SCHEMA,
"GLib or the GSettings schema is not installed; there is no GNOME source here"
);
return Ok(Reading::Absent);
};
Ok(match gsettings_map::configured_mode(&settings)? {
Some(mode) => Reading::configured(ProxyConfigSource::GSettings, mode),
None => {
crate::trace::debug!(
schema = SCHEMA,
"the GSettings schema holds nothing but its defaults; GNOME is unconfigured"
);
Reading::Unset
}
})
}
fn installed_proxy_schema() -> Option<(SchemaSource, Schema)> {
let source = SchemaSource::default(gio_dl::gio()?)?;
let schema = source.lookup(SCHEMA)?;
Some((source, schema))
}
fn child_schema(source: &SchemaSource, parent: &Schema, name: &str) -> Option<Schema> {
if !parent.list_children().iter().any(|child| child == name) {
return None;
}
source.lookup(&format!("{SCHEMA}.{name}"))
}
fn read_settings() -> Option<GnomeSettings> {
let (source, schema) = installed_proxy_schema()?;
let root = schema.settings();
let mut map = GnomeSettings::new();
for (key, kind) in ROOT_KEYS {
read_key(&mut map, &root, &schema, "", key, kind);
}
for child in &CHILDREN {
let Some(child_schema) = child_schema(&source, &schema, child.child) else {
continue;
};
let child_settings = root.child(child.child);
for (key, kind) in CHILD_KEYS {
read_key(
&mut map,
&child_settings,
&child_schema,
child.child,
key,
kind,
);
}
if child.scheme == crate::endpoint::Scheme::Http {
for (key, kind) in HTTP_AUTH_KEYS {
read_key(
&mut map,
&child_settings,
&child_schema,
child.child,
key,
kind,
);
}
if READ_AUTHENTICATION_PASSWORD {
read_key(
&mut map,
&child_settings,
&child_schema,
child.child,
KEY_AUTHENTICATION_PASSWORD,
Kind::Text,
);
}
}
}
Some(map)
}
fn read_key(
map: &mut GnomeSettings,
settings: &Settings,
schema: &Schema,
prefix: &str,
key: &str,
kind: Kind,
) {
if !schema.has_key(key) {
return;
}
let value = match kind {
Kind::Text => GValue::Text(settings.string(key)),
Kind::Flag => GValue::Flag(settings.boolean(key)),
Kind::Int => GValue::Int(settings.int(key)),
Kind::List => GValue::List(settings.strv(key)),
};
let full = if prefix.is_empty() {
key.to_owned()
} else {
GnomeSettings::child_key(prefix, key)
};
if was_written(settings, schema, key) {
map.mark_written(full.clone());
}
map.insert(full, value);
}
fn was_written(settings: &Settings, schema: &Schema, key: &str) -> bool {
settings.user_value(key).is_some()
|| !settings.is_writable(key)
|| settings
.default_value(key)
.is_some_and(|effective| !effective.equals(&schema.key_default_value(key)))
}
pub(crate) struct Handle {
stop: Arc<AtomicBool>,
context: Arc<MainContext>,
thread: Option<JoinHandle<()>>,
}
impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Handle")
.field("stopped", &self.stop.load(Ordering::SeqCst))
.field("running", &self.thread.is_some())
.finish_non_exhaustive()
}
}
impl Handle {
pub(crate) fn start(trigger: SyncSender<()>) -> Result<Option<Self>, Error> {
let Some((source, _)) = installed_proxy_schema() else {
crate::trace::debug!(
schema = SCHEMA,
"GLib or the GSettings proxy schema is not installed; there is nothing to \
subscribe to on this machine"
);
return Ok(None);
};
let context = Arc::new(MainContext::new(source.gio()));
let stop = Arc::new(AtomicBool::new(false));
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), Error>>();
let thread_context = Arc::clone(&context);
let thread_stop = Arc::clone(&stop);
let thread = thread::Builder::new()
.name("proxy-watch-gsettings".to_owned())
.spawn(move || {
thread_context.push_thread_default();
match subscribe(&trigger) {
Ok(settings) => {
let _ = ready_tx.send(Ok(()));
while !thread_stop.load(Ordering::SeqCst) {
thread_context.iteration();
}
drop(settings);
}
Err(error) => {
let _ = ready_tx.send(Err(error));
}
}
thread_context.pop_thread_default();
})
.map_err(|source| Error::io("spawning the proxy-watch GSettings thread", source))?;
match ready_rx.recv() {
Ok(Ok(())) => Ok(Some(Self {
stop,
context,
thread: Some(thread),
})),
Ok(Err(error)) => {
let _ = thread.join();
Err(error)
}
Err(_) => Err(Error::io(
"starting the proxy-watch GSettings thread",
io::Error::other("the watcher thread exited before it became ready"),
)),
}
}
}
impl Drop for Handle {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
self.context.wakeup();
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn subscribe(trigger: &SyncSender<()>) -> Result<Vec<Settings>, Error> {
let missing = |what: &str| {
Error::io(
"subscribing to org.gnome.system.proxy",
io::Error::other(format!("{what} is not installed")),
)
};
let g = gio_dl::gio().ok_or_else(|| missing("GLib"))?;
let source =
SchemaSource::default(g).ok_or_else(|| missing("the default GSettings schema source"))?;
let schema = source.lookup(SCHEMA).ok_or_else(|| missing(SCHEMA))?;
let root = schema.settings();
let children: Vec<Settings> = CHILDREN
.iter()
.filter(|child| child_schema(&source, &schema, child.child).is_some())
.map(|child| root.child(child.child))
.collect();
let mut kept = Vec::with_capacity(children.len() + 1);
for settings in std::iter::once(root).chain(children) {
let changed = trigger.clone();
settings.connect(c"changed", move || {
super::watcher::wake(&changed);
});
let writable = trigger.clone();
settings.connect(c"writable-changed", move || {
super::watcher::wake(&writable);
});
kept.push(settings);
}
crate::trace::debug!(
schemas = kept.len(),
root = SCHEMA,
"subscribed to GSettings::changed and ::writable-changed on the root schema and \
its children"
);
Ok(kept)
}