diff --git a/Cargo.lock b/Cargo.lock index 81befae..f9525ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -736,6 +736,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "displaydoc" version = "0.2.5" @@ -1012,6 +1018,22 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "futures-signals" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70abe9c40a0dccd69bf7c59ba58714ebeb6c15a88143a10c6be7130e895f1696" +dependencies = [ + "discard", + "futures-channel", + "futures-core", + "futures-util", + "gensym", + "log", + "pin-project", + "serde", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1123,6 +1145,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "gensym" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82" +dependencies = [ + "proc-macro2", + "quote 1.0.39", + "syn 2.0.99", + "uuid", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1811,6 +1845,7 @@ dependencies = [ "dirs", "evdev-rs", "futures-lite", + "futures-signals", "glib", "gtk", "gtk-layer-shell", @@ -1834,7 +1869,6 @@ dependencies = [ "sysinfo", "system-tray", "tokio", - "tokio-stream", "tracing", "tracing-appender", "tracing-error", @@ -2507,6 +2541,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote 1.0.39", + "syn 2.0.99", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3503,17 +3557,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.13" @@ -3822,6 +3865,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 366d3b3..8710867 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ music = ["dep:regex"] "music+mpris" = ["music", "mpris"] "music+mpd" = ["music", "mpd-utils"] -network_manager = ["futures-lite", "tokio-stream", "zbus"] +network_manager = ["futures-lite", "futures-signals", "zbus"] notifications = ["zbus"] @@ -171,7 +171,7 @@ regex = { version = "1.11.1", default-features = false, features = [ ], optional = true } # network_manager -tokio-stream = { version = "0.1.17", optional = true } +futures-signals = { version = "0.3.34", optional = true } # sys_info sysinfo = { version = "0.36.1", optional = true } diff --git a/src/clients/mod.rs b/src/clients/mod.rs index cbacaa3..c4fce0d 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -190,7 +190,7 @@ impl Clients { if let Some(client) = &self.network_manager { Ok(client.clone()) } else { - let client = networkmanager::create_client()?; + let client = await_sync(async move { networkmanager::create_client().await })?; self.network_manager = Some(client.clone()); Ok(client) } diff --git a/src/clients/networkmanager.rs b/src/clients/networkmanager.rs new file mode 100644 index 0000000..f81ca45 --- /dev/null +++ b/src/clients/networkmanager.rs @@ -0,0 +1,171 @@ +use std::sync::Arc; + +use crate::{register_fallible_client, spawn}; +use color_eyre::Result; +use futures_signals::signal::{Mutable, MutableSignalCloned}; +use tracing::error; +use zbus::export::ordered_stream::OrderedStreamExt; +use zbus::fdo::PropertiesProxy; +use zbus::{ + Connection, + names::InterfaceName, + proxy, + zvariant::{ObjectPath, Str}, +}; + +const DBUS_BUS: &str = "org.freedesktop.NetworkManager"; +const DBUS_PATH: &str = "/org/freedesktop/NetworkManager"; +const DBUS_INTERFACE: &str = "org.freedesktop.NetworkManager"; + +#[derive(Debug)] +pub struct Client { + client_state: Mutable, + interface_name: InterfaceName<'static>, + dbus_connection: Connection, + props_proxy: PropertiesProxy<'static>, +} + +#[derive(Clone, Debug)] +pub enum ClientState { + WiredConnected, + WifiConnected, + CellularConnected, + VpnConnected, + WifiDisconnected, + Offline, + Unknown, +} + +#[proxy( + default_service = "org.freedesktop.NetworkManager", + interface = "org.freedesktop.NetworkManager", + default_path = "/org/freedesktop/NetworkManager" +)] +trait NetworkManagerDbus { + #[zbus(property)] + fn active_connections(&self) -> Result>; + + #[zbus(property)] + fn devices(&self) -> Result>; + + #[zbus(property)] + fn networking_enabled(&self) -> Result; + + #[zbus(property)] + fn primary_connection(&self) -> Result; + + #[zbus(property)] + fn primary_connection_type(&self) -> Result; + + #[zbus(property)] + fn wireless_enabled(&self) -> Result; +} + +impl Client { + async fn new() -> Result { + let client_state = Mutable::new(ClientState::Unknown); + let dbus_connection = Connection::system().await?; + let interface_name = InterfaceName::from_static_str(DBUS_INTERFACE)?; + let props_proxy = PropertiesProxy::builder(&dbus_connection) + .destination(DBUS_BUS)? + .path(DBUS_PATH)? + .build() + .await?; + + Ok(Self { + client_state, + interface_name, + dbus_connection, + props_proxy, + }) + } + + async fn run(&self) -> Result<()> { + let proxy = NetworkManagerDbusProxy::new(&self.dbus_connection).await?; + + let mut primary_connection = proxy.primary_connection().await?; + let mut primary_connection_type = proxy.primary_connection_type().await?; + let mut wireless_enabled = proxy.wireless_enabled().await?; + + self.client_state.set(determine_state( + &primary_connection, + &primary_connection_type, + wireless_enabled, + )); + + let mut stream = self.props_proxy.receive_properties_changed().await?; + while let Some(change) = stream.next().await { + let args = change.args()?; + if args.interface_name != self.interface_name { + continue; + } + + let changed_props = args.changed_properties; + let mut relevant_prop_changed = false; + + if changed_props.contains_key("PrimaryConnection") { + primary_connection = proxy.primary_connection().await?; + relevant_prop_changed = true; + } + if changed_props.contains_key("PrimaryConnectionType") { + primary_connection_type = proxy.primary_connection_type().await?; + relevant_prop_changed = true; + } + if changed_props.contains_key("WirelessEnabled") { + wireless_enabled = proxy.wireless_enabled().await?; + relevant_prop_changed = true; + } + + if relevant_prop_changed { + self.client_state.set(determine_state( + &primary_connection, + &primary_connection_type, + wireless_enabled, + )); + } + } + + Ok(()) + } + + pub fn subscribe(&self) -> MutableSignalCloned { + self.client_state.signal_cloned() + } +} + +pub async fn create_client() -> Result> { + let client = Arc::new(Client::new().await?); + { + let client = client.clone(); + spawn(async move { + if let Err(error) = client.run().await { + error!("{}", error); + } + }); + } + Ok(client) +} + +fn determine_state( + primary_connection: &str, + primary_connection_type: &str, + wireless_enabled: bool, +) -> ClientState { + if primary_connection == "/" { + if wireless_enabled { + ClientState::WifiDisconnected + } else { + ClientState::Offline + } + } else { + match primary_connection_type { + "802-3-ethernet" | "adsl" | "pppoe" => ClientState::WiredConnected, + "802-11-olpc-mesh" | "802-11-wireless" | "wifi-p2p" => ClientState::WifiConnected, + "cdma" | "gsm" | "wimax" => ClientState::CellularConnected, + "vpn" | "wireguard" => ClientState::VpnConnected, + _ => ClientState::Unknown, + } + } +} + +register_fallible_client!(Client, network_manager); diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs deleted file mode 100644 index 0f1e36a..0000000 --- a/src/clients/networkmanager/dbus.rs +++ /dev/null @@ -1,84 +0,0 @@ -use color_eyre::Result; -use zbus::proxy; -use zbus::zvariant::{ObjectPath, OwnedValue, Str}; - -#[proxy( - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager", - default_path = "/org/freedesktop/NetworkManager" -)] -pub(super) trait Dbus { - #[zbus(property)] - fn all_devices(&self) -> Result>>; -} - -#[proxy( - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.Device" -)] -pub(super) trait DeviceDbus { - #[zbus(property)] - fn device_type(&self) -> Result; - - #[zbus(property)] - fn interface(&self) -> Result>; - - #[zbus(property)] - fn state(&self) -> Result; -} - -// For reference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/e1a7d5ac062f4f23ce3a6b33c62e856056161ad8/src/libnm-core-public/nm-dbus-interface.h#L212-L253 -#[derive(Clone, Debug, Eq, Hash, OwnedValue, PartialEq)] -#[repr(u32)] -pub 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, -} - -// For reference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/e1a7d5ac062f4f23ce3a6b33c62e856056161ad8/src/libnm-core-public/nm-dbus-interface.h#L501-L538 -#[derive(Clone, Debug, OwnedValue, PartialEq)] -#[repr(u32)] -pub 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, -} diff --git a/src/clients/networkmanager/event.rs b/src/clients/networkmanager/event.rs deleted file mode 100644 index 4963f6e..0000000 --- a/src/clients/networkmanager/event.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::clients::networkmanager::dbus::{DeviceState, DeviceType}; - -#[derive(Debug, Clone)] -pub enum Event { - DeviceAdded { - interface: String, - }, - DeviceStateChanged { - interface: String, - r#type: DeviceType, - state: DeviceState, - }, -} diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs deleted file mode 100644 index 94cb82d..0000000 --- a/src/clients/networkmanager/mod.rs +++ /dev/null @@ -1,118 +0,0 @@ -use color_eyre::Result; -use color_eyre::eyre::Ok; -use futures_lite::StreamExt; -use std::collections::HashSet; -use std::sync::Arc; -use tokio::sync::broadcast; -use zbus::Connection; -use zbus::zvariant::{ObjectPath, Str}; - -use crate::clients::ClientResult; -use crate::clients::networkmanager::dbus::{DbusProxy, DeviceDbusProxy}; -use crate::clients::networkmanager::event::Event; -use crate::{register_fallible_client, spawn}; - -pub mod dbus; -pub mod event; - -#[derive(Debug)] -pub struct Client { - tx: broadcast::Sender, -} - -impl Client { - fn new() -> Result { - let (tx, _) = broadcast::channel(64); - Ok(Client { tx }) - } - - fn run(&self) -> Result<()> { - let tx = self.tx.clone(); - spawn(async move { - let dbus_connection = Connection::system().await?; - let root = DbusProxy::new(&dbus_connection).await?; - - let mut devices = HashSet::new(); - - let mut devices_changes = root.receive_all_devices_changed().await; - while let Some(devices_change) = devices_changes.next().await { - // The new list of devices from dbus, not to be confused with the added devices below - let new_devices = HashSet::from_iter(devices_change.get().await?); - - let added_devices = new_devices.difference(&devices); - for added_device in added_devices { - spawn(watch_device(added_device.to_owned(), tx.clone())); - } - - let _removed_devices = devices.difference(&new_devices); - // TODO: Cook up some way to notify closures for removed devices to exit - - devices = new_devices; - } - - Ok(()) - }); - - Ok(()) - } - - pub fn subscribe(&self) -> broadcast::Receiver { - self.tx.subscribe() - } -} - -pub fn create_client() -> ClientResult { - let client = Arc::new(Client::new()?); - client.run()?; - Ok(client) -} - -async fn watch_device(device_path: ObjectPath<'_>, tx: broadcast::Sender) -> Result<()> { - let dbus_connection = Connection::system().await?; - let device = DeviceDbusProxy::new(&dbus_connection, device_path.to_owned()).await?; - - let interface = device.interface().await?; - tx.send(Event::DeviceAdded { - interface: interface.to_string(), - })?; - - spawn(watch_device_state( - device_path.to_owned(), - interface.to_owned(), - tx.clone(), - )); - - Ok(()) -} - -async fn watch_device_state( - device_path: ObjectPath<'_>, - interface: Str<'_>, - tx: broadcast::Sender, -) -> Result<()> { - let dbus_connection = Connection::system().await?; - let device = DeviceDbusProxy::new(&dbus_connection, &device_path).await?; - let r#type = device.device_type().await?; - - // Send an event communicating the initial state - let state = device.state().await?; - tx.send(Event::DeviceStateChanged { - interface: interface.to_string(), - r#type: r#type.clone(), - state, - })?; - - let mut state_changes = device.receive_state_changed().await; - while let Some(state_change) = state_changes.next().await { - let state = state_change.get().await?; - tx.send(Event::DeviceStateChanged { - interface: interface.to_string(), - r#type: r#type.clone(), - state, - })?; - } - - Ok(()) -} - -register_fallible_client!(Client, network_manager); diff --git a/src/modules/networkmanager.rs b/src/modules/networkmanager.rs index ea8b8d9..adb4ff4 100644 --- a/src/modules/networkmanager.rs +++ b/src/modules/networkmanager.rs @@ -1,18 +1,16 @@ -use crate::clients::networkmanager::Client; -use crate::clients::networkmanager::dbus::{DeviceState, DeviceType}; -use crate::clients::networkmanager::event::Event; +use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; +use crate::clients::networkmanager::{Client, ClientState}; use crate::config::CommonConfig; use crate::gtk_helpers::IronbarGtkExt; -use crate::image::Provider; use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext}; use crate::{module_impl, spawn}; -use color_eyre::{Result, eyre::Ok}; -use glib::spawn_future_local; -use gtk::prelude::{ContainerExt, WidgetExt}; -use gtk::{Image, Orientation}; +use color_eyre::Result; +use futures_lite::StreamExt; +use futures_signals::signal::SignalExt; +use gtk::prelude::ContainerExt; +use gtk::{Box as GtkBox, Image}; use serde::Deserialize; -use std::collections::HashMap; -use tokio::sync::{broadcast, mpsc}; +use tokio::sync::mpsc::Receiver; #[derive(Debug, Deserialize, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -28,29 +26,26 @@ const fn default_icon_size() -> i32 { 24 } -impl Module for NetworkManagerModule { - type SendMessage = Event; +impl Module for NetworkManagerModule { + type SendMessage = ClientState; type ReceiveMessage = (); module_impl!("network_manager"); fn spawn_controller( &self, - _info: &ModuleInfo, - context: &WidgetContext, - _rx: mpsc::Receiver<()>, + _: &ModuleInfo, + context: &WidgetContext, + _: Receiver<()>, ) -> Result<()> { let client = context.try_client::()?; - // Should we be using context.tx with ModuleUpdateEvent::Update instead? - let tx = context.update_tx.clone(); - // Must be done here synchronously to avoid race condition - let mut client_rx = client.subscribe(); - spawn(async move { - while let Result::Ok(event) = client_rx.recv().await { - tx.send(event)?; - } + let mut client_signal = client.subscribe().to_stream(); + let tx = context.tx.clone(); - Ok(()) + spawn(async move { + while let Some(state) = client_signal.next().await { + tx.send_update(state).await; + } }); Ok(()) @@ -58,101 +53,50 @@ impl Module for NetworkManagerModule { fn into_widget( self, - context: WidgetContext, - _info: &ModuleInfo, - ) -> Result> { - let container = gtk::Box::new(Orientation::Horizontal, 0); + context: WidgetContext, + info: &ModuleInfo, + ) -> Result> { + const INITIAL_ICON_NAME: &str = "content-loading-symbolic"; - // Must be done here synchronously to avoid race condition - let rx = context.subscribe(); - // We cannot use recv_glib_async here because the lifetimes don't work out - spawn_future_local(handle_update_events( - rx, - container.clone(), - self.icon_size, - context.ironbar.image_provider(), - )); + let container = GtkBox::new(info.bar_position.orientation(), 0); + let icon = Image::new(); + icon.add_class("icon"); + container.add(&icon); + + let image_provider = context.ironbar.image_provider(); + + glib::spawn_future_local({ + let image_provider = image_provider.clone(); + let icon = icon.clone(); + + async move { + image_provider + .load_into_image_silent(INITIAL_ICON_NAME, self.icon_size, false, &icon) + .await; + } + }); + + context.subscribe().recv_glib_async((), move |(), state| { + let image_provider = image_provider.clone(); + let icon = icon.clone(); + + let icon_name = match state { + ClientState::WiredConnected => "network-wired-symbolic", + ClientState::WifiConnected => "network-wireless-symbolic", + ClientState::CellularConnected => "network-cellular-symbolic", + ClientState::VpnConnected => "network-vpn-symbolic", + ClientState::WifiDisconnected => "network-wireless-acquiring-symbolic", + ClientState::Offline => "network-wireless-disabled-symbolic", + ClientState::Unknown => "dialog-question-symbolic", + }; + + async move { + image_provider + .load_into_image_silent(icon_name, self.icon_size, false, &icon) + .await; + } + }); Ok(ModuleParts::new(container, None)) } } - -async fn handle_update_events( - mut rx: broadcast::Receiver, - container: gtk::Box, - icon_size: i32, - image_provider: Provider, -) { - let mut icons = HashMap::::new(); - - while let Result::Ok(event) = rx.recv().await { - match event { - Event::DeviceAdded { interface, .. } => { - let icon = Image::new(); - icon.add_class("icon"); - container.add(&icon); - icons.insert(interface, icon); - } - Event::DeviceStateChanged { - interface, - r#type, - state, - } => { - let icon = icons - .get(&interface) - .expect("the icon for the interface to be present"); - // TODO: Make this configurable at runtime - let icon_name = get_icon_for_device_state(&r#type, &state); - match icon_name { - Some(icon_name) => { - image_provider - .load_into_image_silent(icon_name, icon_size, false, icon) - .await; - icon.show(); - } - None => { - icon.hide(); - } - } - } - }; - } -} - -fn get_icon_for_device_state(r#type: &DeviceType, state: &DeviceState) -> Option<&'static str> { - match r#type { - DeviceType::Ethernet => match state { - DeviceState::Unavailable - | DeviceState::Disconnected - | DeviceState::Prepare - | DeviceState::Config - | DeviceState::NeedAuth - | DeviceState::IpConfig - | DeviceState::IpCheck - | DeviceState::Secondaries - | DeviceState::Deactivating - | DeviceState::Failed => Some("icon:network-wired-disconnected-symbolic"), - DeviceState::Activated => Some("icon:network-wired-symbolic"), - _ => None, - }, - DeviceType::Wifi => match state { - DeviceState::Unavailable => Some("icon:network-wireless-hardware-disabled-symbolic"), - DeviceState::Disconnected - | DeviceState::Prepare - | DeviceState::Config - | DeviceState::NeedAuth - | DeviceState::IpConfig - | DeviceState::IpCheck - | DeviceState::Secondaries - | DeviceState::Deactivating - | DeviceState::Failed => Some("icon:network-wireless-offline-symbolic"), - DeviceState::Activated => Some("icon:network-wireless-connected-symbolic"), - _ => None, - }, - DeviceType::Tun | DeviceType::Wireguard => match state { - DeviceState::Activated => Some("icon:network-vpn-symbolic"), - _ => None, - }, - _ => None, - } -}