From 6b15ac176e7cd585f5ec9bcff51d188ab43e000a Mon Sep 17 00:00:00 2001 From: ralismark <13449732+ralismark@users.noreply.github.com> Date: Sun, 25 Feb 2024 15:36:43 +1100 Subject: [PATCH] Add some documentation to notifier_host --- crates/notifier_host/src/host.rs | 5 +- crates/notifier_host/src/lib.rs | 32 +++++++ crates/notifier_host/src/watcher.rs | 138 ++++++++++++++++------------ 3 files changed, 111 insertions(+), 64 deletions(-) diff --git a/crates/notifier_host/src/host.rs b/crates/notifier_host/src/host.rs index ff60a0d33..ca7bf163a 100644 --- a/crates/notifier_host/src/host.rs +++ b/crates/notifier_host/src/host.rs @@ -95,7 +95,6 @@ pub async fn run_host_forever(host: &mut dyn Host, snw: &dbus::StatusNotifierWat } } - // TODO handle running out of events? why could this happen? - - Ok(()) + // I do not know whether this is possible to reach or not. + unreachable!("StatusNotifierWatcher stopped producing events") } diff --git a/crates/notifier_host/src/lib.rs b/crates/notifier_host/src/lib.rs index a5fe2dd65..37253a63c 100644 --- a/crates/notifier_host/src/lib.rs +++ b/crates/notifier_host/src/lib.rs @@ -1,3 +1,35 @@ +//! The system tray side of the [notifier host DBus +//! protocols](https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierHost/), +//! implementing most of the relevant DBus protocol logic so system tray implementations (e.g. eww) +//! don't need to care about them. +//! +//! This crate does not implement the tray icon side of the protocol. For that, see, for example, +//! the [ksni](https://crates.io/crates/ksni) crate. +//! +//! # Overview / Notes for Contributors +//! +//! This crate makes extensive use of the `zbus` library to interact with DBus. You should read +//! through the [zbus tutorial](https://dbus2.github.io/zbus/) if you aren't familiar with DBus or +//! `zbus`. +//! +//! There are two separate services that are required for the tray side of the protocol: +//! +//! - `StatusNotifierWatcher`, a service which tracks what items and trays there are but doesn't do +//! any rendering. This is implemented by [`Watcher`] (see that for further details), and +//! should always be started alongside the `StatusNotifierHost`. +//! +//! - `StatusNotifierHost`, the actual tray, which registers itself to the StatusNotifierHost and +//! subscribes to its signals to know what items exist. This DBus service has a completely +//! empty interface, but is mainly by StatusNotifierWatcher to know when trays disappear. This +//! is represented by the [`Host`] trait. +//! +//! The actual tray implements the [`Host`] trait to be notified of when items (called +//! `StatusNotifierItem` in the spec and represented by [`Item`]) appear and disappear, then calls +//! [`run_host_forever`] to run the DBus side of the protocol. +//! +//! If there are multiple trays running on the system, there can be multiple `StatusNotifierHost`s, +//! but only one `StatusNotifierWatcher` (usually from whatever tray was started first). + pub mod dbus; mod host; diff --git a/crates/notifier_host/src/watcher.rs b/crates/notifier_host/src/watcher.rs index 8e206ea8b..dc75c8fbf 100644 --- a/crates/notifier_host/src/watcher.rs +++ b/crates/notifier_host/src/watcher.rs @@ -1,65 +1,14 @@ +use crate::*; + use zbus::{dbus_interface, export::ordered_stream::OrderedStreamExt, Interface}; pub const WATCHER_BUS_NAME: &str = "org.kde.StatusNotifierWatcher"; pub const WATCHER_OBJECT_NAME: &str = "/StatusNotifierWatcher"; -async fn parse_service<'a>( - service: &'a str, - hdr: zbus::MessageHeader<'_>, - con: &zbus::Connection, -) -> zbus::fdo::Result<(zbus::names::UniqueName<'static>, &'a str)> { - if service.starts_with('/') { - // they sent us just the object path :( - if let Some(sender) = hdr.sender()? { - Ok((sender.to_owned(), service)) - } else { - log::warn!("unknown sender"); - Err(zbus::fdo::Error::InvalidArgs("Unknown bus address".into())) - } - } else { - let busname: zbus::names::BusName = match service.try_into() { - Ok(x) => x, - Err(e) => { - log::warn!("received invalid bus name {:?}: {}", service, e); - return Err(zbus::fdo::Error::InvalidArgs(e.to_string())); - } - }; - - if let zbus::names::BusName::Unique(unique) = busname { - Ok((unique.to_owned(), "/StatusNotifierItem")) - } else { - let dbus = zbus::fdo::DBusProxy::new(con).await?; - match dbus.get_name_owner(busname).await { - Ok(owner) => Ok((owner.into_inner(), "/StatusNotifierItem")), - Err(e) => { - log::warn!("failed to get owner of {:?}: {}", service, e); - Err(e) - } - } - } - } -} - -/// Wait for a DBus service to exit -async fn wait_for_service_exit(connection: zbus::Connection, service: zbus::names::BusName<'_>) -> zbus::fdo::Result<()> { - let dbus = zbus::fdo::DBusProxy::new(&connection).await?; - let mut owner_changes = dbus.receive_name_owner_changed_with_args(&[(0, &service)]).await?; - - if !dbus.name_has_owner(service.as_ref()).await? { - return Ok(()); - } - - while let Some(sig) = owner_changes.next().await { - let args = sig.args()?; - if args.new_owner().is_none() { - break; - } - } - - Ok(()) -} - -/// An instance of [`org.kde.StatusNotifierWatcher`]. +/// An instance of [`org.kde.StatusNotifierWatcher`]. It only tracks what tray items and trays +/// exist, and doesn't have any logic for displaying items (for that, see [`Host`]). +/// +/// While this is usually run alongside the tray, it can also be used standalone. /// /// [`org.kde.StatusNotifierWatcher`]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/ #[derive(Debug, Default)] @@ -74,6 +23,10 @@ pub struct Watcher { items: std::sync::Arc>>, } +/// Implementation of the `StatusNotifierWatcher` service. +/// +/// Methods and properties correspond to methods and properties on the DBus service that can be +/// used by others, while signals are events that we generate that other services listen to. #[dbus_interface(name = "org.kde.StatusNotifierWatcher")] impl Watcher { /// RegisterStatusNotifierHost method @@ -132,7 +85,7 @@ impl Watcher { Ok(()) } - /// StatusNotifierHostRegistered signal + /// StatusNotifierHostRegistered signal. #[dbus_interface(signal)] async fn status_notifier_host_registered(ctxt: &zbus::SignalContext<'_>) -> zbus::Result<()>; @@ -235,9 +188,10 @@ impl Watcher { /// Attach and run the Watcher on a connection. pub async fn attach_to(self, con: &zbus::Connection) -> zbus::Result<()> { if !con.object_server().at(WATCHER_OBJECT_NAME, self).await? { - // There's already something at this object - // TODO is there a more specific error - return Err(zbus::Error::Failure(format!("Connection already has an object at {}", WATCHER_OBJECT_NAME))); + return Err(zbus::Error::Failure(format!( + "Object already exists at {} on this connection -- is StatusNotifierWatcher already running?", + WATCHER_OBJECT_NAME + ))); } // not AllowReplacement, not ReplaceExisting, not DoNotQueue @@ -272,3 +226,65 @@ impl Watcher { .await } } + +/// Decode the service name that others give to us, into the [bus +/// name](https://dbus2.github.io/zbus/concepts.html#bus-name--service-name) and the [object +/// path](https://dbus2.github.io/zbus/concepts.html#objects-and-object-paths) within the +/// connection. +async fn parse_service<'a>( + service: &'a str, + hdr: zbus::MessageHeader<'_>, + con: &zbus::Connection, +) -> zbus::fdo::Result<(zbus::names::UniqueName<'static>, &'a str)> { + if service.starts_with('/') { + // they sent us just the object path :( + if let Some(sender) = hdr.sender()? { + Ok((sender.to_owned(), service)) + } else { + log::warn!("unknown sender"); + Err(zbus::fdo::Error::InvalidArgs("Unknown bus address".into())) + } + } else { + let busname: zbus::names::BusName = match service.try_into() { + Ok(x) => x, + Err(e) => { + log::warn!("received invalid bus name {:?}: {}", service, e); + return Err(zbus::fdo::Error::InvalidArgs(e.to_string())); + } + }; + + if let zbus::names::BusName::Unique(unique) = busname { + Ok((unique.to_owned(), "/StatusNotifierItem")) + } else { + let dbus = zbus::fdo::DBusProxy::new(con).await?; + match dbus.get_name_owner(busname).await { + Ok(owner) => Ok((owner.into_inner(), "/StatusNotifierItem")), + Err(e) => { + log::warn!("failed to get owner of {:?}: {}", service, e); + Err(e) + } + } + } + } +} + +/// Wait for a DBus service to exit +async fn wait_for_service_exit(connection: zbus::Connection, service: zbus::names::BusName<'_>) -> zbus::fdo::Result<()> { + // TODO do we also want to catch when the object disappears? + + let dbus = zbus::fdo::DBusProxy::new(&connection).await?; + let mut owner_changes = dbus.receive_name_owner_changed_with_args(&[(0, &service)]).await?; + + if !dbus.name_has_owner(service.as_ref()).await? { + return Ok(()); + } + + while let Some(sig) = owner_changes.next().await { + let args = sig.args()?; + if args.new_owner().is_none() { + break; + } + } + + Ok(()) +}