diff --git a/docs/modules/Networkmanager.md b/docs/modules/Networkmanager.md index ec3a8fd..bd318d9 100644 --- a/docs/modules/Networkmanager.md +++ b/docs/modules/Networkmanager.md @@ -69,6 +69,6 @@ Supports wired ethernet, wifi, cellular data and VPN connections among others. | Selector | Description | |------------------------|----------------------------------| | `.networkmanager` | NetworkManager widget container. | -| `.networkmanger .icon` | NetworkManager widget icon. | +| `.networkmanger .icon` | NetworkManager widget icons. | For more information on styling, please see the [styling guide](styling-guide). diff --git a/src/clients/networkmanager.rs b/src/clients/networkmanager.rs deleted file mode 100644 index 62ab8e6..0000000 --- a/src/clients/networkmanager.rs +++ /dev/null @@ -1,296 +0,0 @@ -use std::ops::Deref; -use std::sync::{Arc, RwLock}; - -use color_eyre::Result; -use futures_signals::signal::{Mutable, MutableSignalCloned}; -use tracing::error; -use zbus::blocking::Connection; -use zbus::{ - dbus_proxy, - zvariant::{ObjectPath, Str}, -}; - -use crate::{ - read_lock, register_fallible_client, spawn_blocking, spawn_blocking_result, write_lock, -}; - -// States - -#[derive(Clone, Debug)] -pub struct State { - pub wired: WiredState, - pub wifi: WifiState, - pub cellular: CellularState, - pub vpn: VpnState, -} - -#[derive(Clone, Debug)] -pub enum WiredState { - Connected, - Disconnected, - NotPresent, - Unknown, -} - -#[derive(Clone, Debug)] -pub enum WifiState { - Connected(WifiConnectedState), - Disconnected, - Disabled, - NotPresent, - Unknown, -} - -#[derive(Clone, Debug)] -pub struct WifiConnectedState { - pub ssid: String, -} - -#[derive(Clone, Debug)] -pub enum CellularState { - Connected, - Disconnected, - Disabled, - NotPresent, - Unknown, -} - -#[derive(Clone, Debug)] -pub enum VpnState { - Connected(VpnConnectedState), - Disconnected, - Unknown, -} - -#[derive(Clone, Debug)] -pub struct VpnConnectedState { - pub name: String, -} - -// D-Bus interfaces - -#[dbus_proxy( - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager", - default_path = "/org/freedesktop/NetworkManager" -)] -trait Dbus { - #[dbus_proxy(property)] - fn active_connections(&self) -> Result>; - - #[dbus_proxy(property)] - fn devices(&self) -> Result>; - - #[dbus_proxy(property)] - fn networking_enabled(&self) -> Result; - - #[dbus_proxy(property)] - fn primary_connection(&self) -> Result; - - #[dbus_proxy(property)] - fn primary_connection_type(&self) -> Result; - - #[dbus_proxy(property)] - fn wireless_enabled(&self) -> Result; -} - -#[dbus_proxy( - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.Connection.Active" -)] -trait ActiveConnectionDbus { - #[dbus_proxy(property)] - fn connection(&self) -> Result; - - #[dbus_proxy(property)] - fn devices(&self) -> Result>; - - #[dbus_proxy(property)] - fn id(&self) -> Result; - - #[dbus_proxy(property)] - fn specific_object(&self) -> Result; - - #[dbus_proxy(property)] - fn type_(&self) -> Result; - - #[dbus_proxy(property)] - fn uuid(&self) -> Result; -} - -// Ironbar client & helpers - -#[derive(Debug)] -pub struct Client(Arc>); - -#[derive(Debug)] -struct ClientInner<'a> { - state: Mutable, - - dbus_proxy: &'a DbusProxyBlocking<'a>, - - primary_connection: RwLock>, - primary_connection_type: RwLock>, - wireless_enabled: RwLock, -} - -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 dbus_proxy = DbusProxyBlocking::new(&dbus_connection)?; - let primary_connection = dbus_proxy.primary_connection()?.to_owned(); - let primary_connection_type = dbus_proxy.primary_connection_type()?.to_owned(); - let wireless_enabled = dbus_proxy.wireless_enabled()?; - - Ok(Client(Arc::new(ClientInner { - state, - dbus_proxy: Box::leak(Box::new(dbus_proxy)), - primary_connection: RwLock::new(primary_connection), - primary_connection_type: RwLock::new(primary_connection_type), - wireless_enabled: RwLock::new(wireless_enabled), - }))) - } - - fn run(&self) -> Result<()> { - macro_rules! spawn_property_watcher { - ($client:expr, $property:ident, $property_changes:ident) => { - let client = $client.clone(); - spawn_blocking_result!({ - while let Some(change) = client.dbus_proxy.$property_changes().next() { - { - let new_value = change.get()?; - let mut value_guard = write_lock!(client.$property); - *value_guard = new_value; - } - client.state.set(determine_state( - read_lock!(client.primary_connection).deref(), - read_lock!(client.primary_connection_type).deref(), - *read_lock!(client.wireless_enabled), - )); - } - Ok(()) - }); - }; - } - - // Initial state - self.0.state.set(determine_state( - &read_lock!(self.0.primary_connection), - &read_lock!(self.0.primary_connection_type), - *read_lock!(self.0.wireless_enabled), - )); - - spawn_property_watcher!( - self.0, - primary_connection, - receive_primary_connection_changed - ); - spawn_property_watcher!( - self.0, - primary_connection_type, - receive_primary_connection_type_changed - ); - spawn_property_watcher!(self.0, wireless_enabled, receive_wireless_enabled_changed); - - 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) -} - -fn determine_state( - primary_connection: &ObjectPath, - primary_connection_type: &Str, - wireless_enabled: bool, -) -> State { - if primary_connection == "/" { - if wireless_enabled { - State { - wired: WiredState::Unknown, - wifi: WifiState::Disconnected, - cellular: CellularState::Unknown, - vpn: VpnState::Unknown, - } - } else { - State { - wired: WiredState::Unknown, - wifi: WifiState::Disabled, - cellular: CellularState::Unknown, - vpn: VpnState::Unknown, - } - } - } else { - match primary_connection_type.as_str() { - "802-3-ethernet" | "adsl" | "pppoe" => State { - wired: WiredState::Connected, - wifi: WifiState::Unknown, - cellular: CellularState::Unknown, - vpn: VpnState::Unknown, - }, - "802-11-olpc-mesh" | "802-11-wireless" | "wifi-p2p" => State { - wired: WiredState::Unknown, - wifi: WifiState::Connected(WifiConnectedState { - ssid: String::new(), - }), - cellular: CellularState::Unknown, - vpn: VpnState::Unknown, - }, - "cdma" | "gsm" | "wimax" => State { - wired: WiredState::Unknown, - wifi: WifiState::Unknown, - cellular: CellularState::Connected, - vpn: VpnState::Unknown, - }, - "vpn" | "wireguard" => State { - wired: WiredState::Unknown, - wifi: WifiState::Unknown, - cellular: CellularState::Unknown, - vpn: VpnState::Connected(VpnConnectedState { - name: String::new(), - }), - }, - _ => State { - wired: WiredState::Unknown, - wifi: WifiState::Unknown, - cellular: CellularState::Unknown, - vpn: VpnState::Unknown, - }, - } - } -} - -// fn instantiate_active_connections<'a>( -// dbus_connection: &Connection, -// active_connection_paths: Vec, -// ) -> Result>> { -// let mut active_connections = Vec::new(); -// for active_connection_path in active_connection_paths { -// let active_connection_proxy = ActiveConnectionDbusProxyBlocking::builder(dbus_connection) -// .path(active_connection_path)? -// .build()?; -// active_connections.push(active_connection_proxy); -// } -// Ok(active_connections) -// } - -register_fallible_client!(Client, networkmanager); diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs new file mode 100644 index 0000000..d9f7062 --- /dev/null +++ b/src/clients/networkmanager/dbus.rs @@ -0,0 +1,133 @@ +use color_eyre::Result; +use zbus::dbus_proxy; +use zbus::zvariant::{ObjectPath, OwnedValue, Str}; + +#[dbus_proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +trait Dbus { + #[dbus_proxy(property)] + fn active_connections(&self) -> Result>; + + #[dbus_proxy(property)] + fn devices(&self) -> Result>; + + #[dbus_proxy(property)] + fn networking_enabled(&self) -> Result; + + #[dbus_proxy(property)] + fn primary_connection(&self) -> Result; + + #[dbus_proxy(property)] + fn primary_connection_type(&self) -> Result; + + #[dbus_proxy(property)] + fn wireless_enabled(&self) -> Result; +} + +#[dbus_proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Connection.Active" +)] +trait ActiveConnectionDbus { + #[dbus_proxy(property)] + fn connection(&self) -> Result; + + #[dbus_proxy(property)] + fn default(&self) -> Result; + + #[dbus_proxy(property)] + fn default6(&self) -> Result; + + #[dbus_proxy(property)] + fn devices(&self) -> Result>; + + #[dbus_proxy(property)] + fn id(&self) -> Result; + + #[dbus_proxy(property)] + fn type_(&self) -> Result; + + #[dbus_proxy(property)] + fn uuid(&self) -> Result; +} + +#[dbus_proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager.Device" +)] +trait DeviceDbus { + #[dbus_proxy(property)] + fn active_connection(&self) -> Result; + + #[dbus_proxy(property)] + fn device_type(&self) -> Result; + + #[dbus_proxy(property)] + fn state(&self) -> Result; +} + +#[derive(Clone, Debug, OwnedValue, PartialEq)] +#[repr(u32)] +pub(super) enum DeviceType { + Unknown = 0, + Ethernet = 1, + Wifi = 2, + Bluetooth = 5, + OlpcMesh = 6, + Wimax = 7, + Modem = 8, + Infiniband = 9, + Bond = 10, + Vlan = 11, + Adsl = 12, + Bridge = 13, + Team = 15, + Tun = 16, + IpTunnel = 17, + Macvlan = 18, + Vxlan = 19, + Veth = 20, + Macsec = 21, + Dummy = 22, + Ppp = 23, + OvsInterface = 24, + OvsPort = 25, + OvsBridge = 26, + Wpan = 27, + Lowpan = 28, + Wireguard = 29, + WifiP2p = 30, + Vrf = 31, + Loopback = 32, + Hsr = 33, +} + +#[derive(Clone, Debug, OwnedValue, PartialEq)] +#[repr(u32)] +pub(super) enum DeviceState { + Unknown = 0, + Unmanaged = 10, + Unavailable = 20, + Disconnected = 30, + Prepare = 40, + Config = 50, + NeedAuth = 60, + IpConfig = 70, + IpCheck = 80, + Secondaries = 90, + Activated = 100, + Deactivating = 110, + Failed = 120, +} + +impl DeviceState { + pub(super) fn is_enabled(&self) -> bool { + match self { + DeviceState::Unknown | DeviceState::Unmanaged | DeviceState::Unavailable => false, + _ => true, + } + } +} diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs new file mode 100644 index 0000000..97c58c7 --- /dev/null +++ b/src/clients/networkmanager/mod.rs @@ -0,0 +1,217 @@ +mod dbus; +pub mod state; + +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, +}; + +type PathMap<'a, T> = HashMap, T>; + +#[derive(Debug)] +pub struct Client(Arc>); + +#[derive(Debug)] +struct ClientInner<'a> { + state: Mutable, + root_object: &'a DbusProxyBlocking<'a>, + 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 dbus_proxy = DbusProxyBlocking::new(&dbus_connection)?; + + Ok(Client(Arc::new(ClientInner { + state, + root_object: Box::leak(Box::new(dbus_proxy)), // TODO: Check if boxing is still necessary + active_connections: RwLock::new(HashMap::new()), + devices: RwLock::new(HashMap::new()), + dbus_connection, + }))) + } + + fn run(&self) -> Result<()> { + macro_rules! spawn_property_watcher { + ($client:expr, $property_changes:ident) => { + let client = $client.clone(); + spawn_blocking_result!({ + while let Some(_) = client.root_object.$property_changes().next() { + client.state.set(State { + 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_list_watcher { + () => {}; + } + + // Initial state + 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))?, + }); + + // Active connections paths list watcher + { + let client = self.0.clone(); + spawn_blocking_result!({ + let mut changes = client.root_object.receive_active_connections_changed(); + while let Some(_) = changes.next() { + let mut new_pathmap = HashMap::new(); + { + let new_paths = client.root_object.active_connections()?; + let active_connections = read_lock!(client.active_connections); + for new_path in new_paths { + if active_connections.contains_key(&new_path) { + let proxy = active_connections + .get(&new_path) + .expect("Should contain the key, see check above"); + new_pathmap.insert(new_path, proxy.clone()); + } else { + let new_proxy = ActiveConnectionDbusProxyBlocking::builder( + &client.dbus_connection, + ) + .path(new_path.clone())? + .build()?; + new_pathmap.insert(new_path, new_proxy); + + // Active connection type is assumed to never change + } + } + } + *write_lock!(client.active_connections) = new_pathmap; + client.state.set(State { + 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(()) + }); + } + + // Devices paths list watcher + { + let client = self.0.clone(); + spawn_blocking_result!({ + let mut changes = client.root_object.receive_devices_changed(); + while let Some(_) = changes.next() { + let mut new_pathmap = HashMap::new(); + { + let new_paths = client.root_object.devices()?; + let devices = read_lock!(client.devices); + for new_path in new_paths { + if devices.contains_key(&new_path) { + let proxy = devices + .get(&new_path) + .expect("Should contain the key, see check above"); + new_pathmap.insert(new_path, proxy.clone()); + } else { + let new_proxy = + DeviceDbusProxyBlocking::builder(&client.dbus_connection) + .path(new_path.clone())? + .build()?; + + // Specific device state watcher + { + let client = client.clone(); + let new_path = new_path.clone(); + spawn_blocking_result!({ + let mut changes = read_lock!(client.devices) + .get(&new_path) + .unwrap() + .receive_state_changed(); + while let Some(_) = changes.next() { + client.state.set(State { + 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(()) + }); + } + + // Device type is assumed to never change + + new_pathmap.insert(new_path, new_proxy); + } + } + } + *write_lock!(client.devices) = new_pathmap; + client.state.set(State { + 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(()) + }); + } + + 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); diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs new file mode 100644 index 0000000..e5e226e --- /dev/null +++ b/src/clients/networkmanager/state.rs @@ -0,0 +1,163 @@ +use color_eyre::Result; + +use crate::clients::networkmanager::dbus::{ + ActiveConnectionDbusProxyBlocking, DeviceDbusProxyBlocking, DeviceState, DeviceType, +}; +use crate::clients::networkmanager::PathMap; + +#[derive(Clone, Debug)] +pub struct State { + pub wired: WiredState, + pub wifi: WifiState, + pub cellular: CellularState, + pub vpn: VpnState, +} + +#[derive(Clone, Debug)] +pub enum WiredState { + Connected, + Disconnected, + NotPresent, + Unknown, +} + +#[derive(Clone, Debug)] +pub enum WifiState { + Connected(WifiConnectedState), + Disconnected, + Disabled, + NotPresent, + Unknown, +} + +#[derive(Clone, Debug)] +pub struct WifiConnectedState { + pub ssid: String, +} + +#[derive(Clone, Debug)] +pub enum CellularState { + Connected, + Disconnected, + Disabled, + NotPresent, + Unknown, +} + +#[derive(Clone, Debug)] +pub enum VpnState { + Connected(VpnConnectedState), + Disconnected, + Unknown, +} + +#[derive(Clone, Debug)] +pub struct VpnConnectedState { + pub name: String, +} + +pub(super) fn determine_wired_state( + devices: &PathMap, +) -> Result { + let mut present = false; + let mut connected = false; + + for device in devices.values() { + if device.device_type()? == DeviceType::Ethernet { + present = true; + if device.state()?.is_enabled() { + connected = true; + break; + } + } + } + + if connected { + Ok(WiredState::Connected) + } else if present { + Ok(WiredState::Disconnected) + } else { + Ok(WiredState::NotPresent) + } +} + +pub(super) fn determine_wifi_state( + devices: &PathMap, +) -> Result { + let mut present = false; + let mut enabled = false; + let mut connected = false; + + for device in devices.values() { + if device.device_type()? == DeviceType::Wifi { + present = true; + if device.state()?.is_enabled() { + enabled = true; + if device.state()? == DeviceState::Activated { + connected = true; + break; + } + } + } + } + + if connected { + Ok(WifiState::Connected(WifiConnectedState { + // TODO: Implement obtaining SSID + ssid: "unknown".into(), + })) + } else if enabled { + Ok(WifiState::Disconnected) + } else if present { + Ok(WifiState::Disabled) + } else { + Ok(WifiState::NotPresent) + } +} + +pub(super) fn determine_cellular_state( + devices: &PathMap, +) -> Result { + let mut present = false; + let mut enabled = false; + let mut connected = false; + + for device in devices.values() { + if device.device_type()? == DeviceType::Modem { + present = true; + if device.state()?.is_enabled() { + enabled = true; + if device.state()? == DeviceState::Activated { + connected = true; + break; + } + } + } + } + + if connected { + Ok(CellularState::Connected) + } else if enabled { + Ok(CellularState::Disconnected) + } else if present { + Ok(CellularState::Disabled) + } else { + Ok(CellularState::NotPresent) + } +} + +pub(super) fn determine_vpn_state( + active_connections: &PathMap, +) -> Result { + for connection in active_connections.values() { + match connection.type_()?.as_str() { + "vpn" | "wireguard" => { + return Ok(VpnState::Connected(VpnConnectedState { + name: "unknown".into(), + })); + } + _ => {} + } + } + Ok(VpnState::Disconnected) +} diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index f608110..681f7b5 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -6,7 +6,10 @@ use gtk::{Box as GtkBox, Image, Orientation}; use serde::Deserialize; use tokio::sync::mpsc::Receiver; -use crate::clients::networkmanager::{CellularState, Client, State, VpnState, WifiState, WiredState}; +use crate::clients::networkmanager::state::{ + CellularState, State, VpnState, WifiState, WiredState, +}; +use crate::clients::networkmanager::Client; use crate::config::CommonConfig; use crate::gtk_helpers::IronbarGtkExt; use crate::image::ImageProvider; @@ -59,28 +62,36 @@ impl Module for NetworkManagerModule { // Wired icon let wired_icon = Image::new(); + wired_icon.add_class("icon"); wired_icon.add_class("wired-icon"); container.add(&wired_icon); // Wifi icon let wifi_icon = Image::new(); + wifi_icon.add_class("icon"); wifi_icon.add_class("wifi-icon"); container.add(&wifi_icon); // Cellular icon let cellular_icon = Image::new(); + cellular_icon.add_class("icon"); cellular_icon.add_class("cellular-icon"); container.add(&cellular_icon); // VPN icon let vpn_icon = Image::new(); + vpn_icon.add_class("icon"); vpn_icon.add_class("vpn-icon"); container.add(&vpn_icon); let icon_theme = info.icon_theme.clone(); glib_recv!(context.subscribe(), state => { macro_rules! update_icon { - ($icon_var:expr, $state_type:ident, {$($state:pat => $icon_name:expr,)+}) => { + ( + $icon_var:expr, + $state_type:ident, + {$($state:pat => $icon_name:expr,)+} + ) => { let icon_name = match state.$state_type { $($state => $icon_name,)+ }; @@ -113,7 +124,8 @@ impl Module for NetworkManagerModule { }); update_icon!(vpn_icon, vpn, { VpnState::Connected(_) => "icon:network-vpn-symbolic", - VpnState::Disconnected | VpnState::Unknown => "", + VpnState::Disconnected => "icon:network-vpn-disabled-symbolic", + VpnState::Unknown => "", }); });