use std::collections::HashMap; use std::sync::{Arc, RwLock}; use color_eyre::Result; use futures_signals::signal::{Mutable, MutableSignalCloned}; use tracing::error; use zbus::blocking::Connection; use zbus::zvariant::ObjectPath; use crate::clients::networkmanager::dbus::{ ActiveConnectionDbusProxyBlocking, DbusProxyBlocking, DeviceDbusProxyBlocking, }; use crate::clients::networkmanager::state::{ determine_cellular_state, determine_vpn_state, determine_wifi_state, determine_wired_state, CellularState, State, VpnState, WifiState, WiredState, }; use crate::{ read_lock, register_fallible_client, spawn_blocking, spawn_blocking_result, write_lock, }; mod dbus; pub mod state; type PathMap<'l, ValueType> = HashMap, ValueType>; #[derive(Debug)] pub struct Client(Arc>); #[derive(Debug)] struct ClientInner<'l> { state: Mutable, root_object: &'l DbusProxyBlocking<'l>, active_connections: RwLock>>, devices: RwLock>>, dbus_connection: Connection, } impl Client { fn new() -> Result { let state = Mutable::new(State { wired: WiredState::Unknown, wifi: WifiState::Unknown, cellular: CellularState::Unknown, vpn: VpnState::Unknown, }); let dbus_connection = Connection::system()?; let root_object = { let root_object = DbusProxyBlocking::new(&dbus_connection)?; // Workaround for the fact that zbus (unnecessarily) requires a static lifetime here Box::leak(Box::new(root_object)) }; Ok(Client(Arc::new(ClientInner { state, root_object, active_connections: RwLock::new(HashMap::new()), devices: RwLock::new(HashMap::new()), dbus_connection, }))) } fn run(&self) -> Result<()> { macro_rules! spawn_path_list_watcher { ( $client:expr, $property:ident, $property_changes:ident, $proxy_type:ident, $(|$inner_client:ident, $new_path:ident| $property_watcher:expr,)* ) => { let client = $client.clone(); spawn_blocking_result!({ let changes = client.root_object.$property_changes(); for _ in changes { let mut new_pathmap = HashMap::new(); { let new_paths = client.root_object.$property()?; let pathmap = read_lock!(client.$property); for new_path in new_paths { if pathmap.contains_key(&new_path) { let proxy = pathmap .get(&new_path) .expect("Should contain the key, guarded by runtime check"); new_pathmap.insert(new_path, proxy.to_owned()); } else { let new_proxy = $proxy_type::builder(&client.dbus_connection) .path(new_path.clone())? .build()?; new_pathmap.insert(new_path.clone(), new_proxy); $({ let $inner_client = &client; let $new_path = &new_path; $property_watcher })* } } } *write_lock!(client.$property) = new_pathmap; client.state.set(State { // TODO: Investigate if there's a sane way to do only the relevant updates wired: determine_wired_state(&read_lock!(client.devices))?, wifi: determine_wifi_state(&read_lock!(client.devices))?, cellular: determine_cellular_state(&read_lock!(client.devices))?, vpn: determine_vpn_state(&read_lock!(client.active_connections))?, }); } Ok(()) }); } } macro_rules! spawn_property_watcher { ( $client:expr, $path:expr, $property_changes:ident, $containing_list:ident ) => { let client = $client.clone(); let path = $path.clone(); spawn_blocking_result!({ let changes = read_lock!(client.$containing_list) .get(&path) .expect("Should contain the key upon watcher start") .$property_changes(); for _ in changes { if !read_lock!(client.$containing_list).contains_key(&path) { break; } client.state.set(State { // TODO: Investigate if there's a sane way to do only the relevant updates wired: determine_wired_state(&read_lock!(client.devices))?, wifi: determine_wifi_state(&read_lock!(client.devices))?, cellular: determine_cellular_state(&read_lock!(client.devices))?, vpn: determine_vpn_state(&read_lock!(client.active_connections))?, }); } Ok(()) }); }; } // Initialisation { // Initial active connections path list { let new_paths = self.0.root_object.active_connections()?; let mut pathmap = write_lock!(self.0.active_connections); for new_path in new_paths { let new_proxy = ActiveConnectionDbusProxyBlocking::builder(&self.0.dbus_connection) .path(new_path.clone())? .build()?; pathmap.insert(new_path, new_proxy); } } // Initial devices path list { let new_paths = self.0.root_object.devices()?; let mut pathmap = write_lock!(self.0.devices); for new_path in new_paths { let new_proxy = DeviceDbusProxyBlocking::builder(&self.0.dbus_connection) .path(new_path.clone())? .build()?; pathmap.insert(new_path.clone(), new_proxy); spawn_property_watcher!(self.0, new_path, receive_state_changed, devices); } } self.0.state.set(State { wired: determine_wired_state(&read_lock!(self.0.devices))?, wifi: determine_wifi_state(&read_lock!(self.0.devices))?, cellular: determine_cellular_state(&read_lock!(self.0.devices))?, vpn: determine_vpn_state(&read_lock!(self.0.active_connections))?, }); } spawn_path_list_watcher!( self.0, active_connections, receive_active_connections_changed, ActiveConnectionDbusProxyBlocking, ); spawn_path_list_watcher!( self.0, devices, receive_devices_changed, DeviceDbusProxyBlocking, |client, path| { spawn_property_watcher!(client, path, receive_state_changed, devices); }, ); Ok(()) } pub fn subscribe(&self) -> MutableSignalCloned { self.0.state.signal_cloned() } } pub fn create_client() -> Result> { let client = Arc::new(Client::new()?); { let client = client.clone(); spawn_blocking_result!({ client.run()?; Ok(()) }); } Ok(client) } register_fallible_client!(Client, networkmanager);