Skip to content

Commit

Permalink
Add some documentation to notifier_host
Browse files Browse the repository at this point in the history
  • Loading branch information
ralismark committed Feb 25, 2024
1 parent 5b507c8 commit 6b15ac1
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 64 deletions.
5 changes: 2 additions & 3 deletions crates/notifier_host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
32 changes: 32 additions & 0 deletions crates/notifier_host/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
138 changes: 77 additions & 61 deletions crates/notifier_host/src/watcher.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -74,6 +23,10 @@ pub struct Watcher {
items: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
}

/// 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
Expand Down Expand Up @@ -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<()>;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(())
}

0 comments on commit 6b15ac1

Please sign in to comment.