Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions crates/eww/src/widgets/systray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{cell::RefCell, future::Future, rc::Rc};

// DBus state shared between systray instances, to avoid creating too many connections etc.
struct DBusSession {
snw: notifier_host::proxy::StatusNotifierWatcherProxy<'static>,
con: zbus::Connection,
}

async fn dbus_session() -> zbus::Result<&'static DBusSession> {
Expand All @@ -22,9 +22,7 @@ async fn dbus_session() -> zbus::Result<&'static DBusSession> {
let con = zbus::Connection::session().await?;
notifier_host::Watcher::new().attach_to(&con).await?;

let (_, snw) = notifier_host::register_as_host(&con).await?;

Ok(DBusSession { snw })
Ok(DBusSession { con })
})
.await
}
Expand Down Expand Up @@ -83,7 +81,7 @@ pub fn spawn_systray(container: &gtk::Box, props: &Props) {
};

systray.container.show();
let e = notifier_host::run_host(&mut systray, &s.snw).await;
let e = notifier_host::run_host_forever(&mut systray, &s.con).await;
log::error!("notifier host error: {}", e);
});

Expand Down Expand Up @@ -114,6 +112,12 @@ impl notifier_host::Host for Tray {
log::warn!("Tried to remove nonexistent item {:?} from systray", id);
}
}

fn clear(&mut self) {
for (_, item) in self.items.drain() {
self.container.remove(&item.widget);
}
}
}

/// Item represents a single icon being shown in the system tray.
Expand Down
104 changes: 97 additions & 7 deletions crates/notifier_host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ pub trait Host {

/// Called when an item is removed from the tray.
fn remove_item(&mut self, id: &str);

/// Called by [`run_host_forever`] before re-bootstrapping against a new StatusNotifierWatcher
/// owner. Implementations should remove all currently-displayed items; they will be
/// re-enumerated against the new owner.
fn clear(&mut self);
}

// TODO We aren't really thinking about what happens when we shut down a host. Currently, we don't
// provide a way to unregister as a host.
// The StatusNotifier spec defines no method to unregister a host; a host's lifetime is tracked
// implicitly by the watcher watching its well-known name, so the name is held for the lifetime of
// the connection and released only when the connection drops. Accordingly we acquire a single
// stable host name per connection and reuse it across re-registrations (see `register_as_host`).
//
// It would also be good to combine `register_as_host` and `run_host`, so that we're only
// registered while we're running.

/// Register this DBus connection as a StatusNotifierHost (i.e. system tray).
///
/// This associates with the DBus connection new name of the format
/// `org.freedesktop.StatusNotifierHost-{pid}-{nr}`, and registers it to active
/// This associates the DBus connection with a name of the format
/// `org.freedesktop.StatusNotifierHost-{pid}-{nr}`, and registers it to the active
/// StatusNotifierWatcher. The name and the StatusNotifierWatcher proxy are returned.
///
/// If the connection already owns such a host name (e.g. from a previous call), that name is
/// reused rather than a fresh one being claimed, so repeated registrations on one connection do
/// not accumulate host names on the bus.
///
/// You still need to call [`run_host`] to have the instance of [`Host`] be notified of new and
/// removed items.
pub async fn register_as_host(
Expand All @@ -43,9 +54,10 @@ pub async fn register_as_host(

let flags = [zbus::fdo::RequestNameFlags::DoNotQueue];
match con.request_name_with_flags(&wellknown, flags.into_iter().collect()).await? {
PrimaryOwner => break wellknown,
Exists => {}
AlreadyOwner => {}
// Reuse a name we already hold instead of claiming a fresh one, so repeated
// registrations on this connection don't accumulate host names on the bus.
PrimaryOwner | AlreadyOwner => break wellknown,
Exists => {} // owned by another connection; try the next index
InQueue => unreachable!("request_name_with_flags returned InQueue even though we specified DoNotQueue"),
};
};
Expand Down Expand Up @@ -133,3 +145,81 @@ pub async fn run_host(host: &mut dyn Host, snw: &proxy::StatusNotifierWatcherPro
// I do not know whether this is possible to reach or not.
unreachable!("StatusNotifierWatcher stopped producing events")
}

/// Run a Host indefinitely, re-bootstrapping host registration and signal subscriptions whenever
/// the `org.kde.StatusNotifierWatcher` well-known name changes ownership.
///
/// This is the recommended entry point for hosts that may start before the system's
/// StatusNotifierWatcher is fully owned by a single stable process (e.g. when a transient
/// fallback watcher like libayatana-appindicator's claims the name during graphical session
/// startup, then exits when a longer-lived watcher takes over).
pub async fn run_host_forever(host: &mut dyn Host, con: &zbus::Connection) -> zbus::Error {
macro_rules! try_ {
($e:expr) => {
match $e {
Ok(x) => x,
Err(e) => return e.into(),
}
};
}

let dbus = try_!(zbus::fdo::DBusProxy::new(con).await);
let watcher_bus = zbus::names::BusName::try_from(crate::names::WATCHER_BUS)
.expect("WATCHER_BUS is a valid well-known name");

loop {
let mut owner_changes = try_!(dbus.receive_name_owner_changed_with_args(&[(0, &watcher_bus)]).await);

// Block until the watcher name has an owner so `register_as_host` has something to call.
// Handles both the initial-startup case (no watcher yet) and a fast owner-flap where the
// previous owner has departed but no replacement has claimed the name yet.
let initial_owner = loop {
match dbus.get_name_owner(watcher_bus.as_ref()).await {
Ok(name) => break name.to_string(),
Err(zbus::fdo::Error::NameHasNoOwner(_)) => {
while let Some(sig) = owner_changes.next().await {
let args = match sig.args() {
Ok(a) => a,
Err(_) => continue,
};
if args.new_owner().is_some() {
break;
}
}
}
Err(e) => return e.into(),
}
};

log::debug!("clearing tray items before (re-)bootstrap against watcher owner {}", initial_owner);
host.clear();

let (_, snw) = try_!(register_as_host(con).await);

let owner_changed = async {
while let Some(sig) = owner_changes.next().await {
let args = match sig.args() {
Ok(a) => a,
Err(_) => continue,
};
let new_owner = args.new_owner().as_ref().map(|n| n.to_string());

if new_owner.as_deref() != Some(initial_owner.as_str()) {
return;
}
}
};

// Exiting the select drops `snw` and its signal subscriptions, so the next iteration
// rebinds against whoever owns the watcher name then.
tokio::select! {
err = run_host(host, &snw) => return err,
_ = owner_changed => {
log::info!(
"StatusNotifierWatcher ownership changed (was {}); re-bootstrapping host",
initial_owner
);
}
}
}
}
75 changes: 67 additions & 8 deletions crates/notifier_host/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ use zbus::{dbus_interface, export::ordered_stream::OrderedStreamExt, Interface};
/// [`org.kde.StatusNotifierWatcher`]: https://freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierWatcher/
#[derive(Debug, Default)]
pub struct Watcher {
tasks: tokio::task::JoinSet<()>,

// Intentionally using std::sync::Mutex instead of tokio's async mutex, since we don't need to
// hold the mutex across an await.
//
// See <https://docs.rs/tokio/latest/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
tasks: std::sync::Arc<std::sync::Mutex<tokio::task::JoinSet<()>>>,
hosts: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
items: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
}
Expand Down Expand Up @@ -58,7 +57,7 @@ impl Watcher {
}
Watcher::status_notifier_host_registered(&ctxt).await?;

self.tasks.spawn({
self.tasks.lock().unwrap().spawn({
let hosts = self.hosts.clone();
let ctxt = ctxt.to_owned();
let con = con.to_owned();
Expand Down Expand Up @@ -131,7 +130,7 @@ impl Watcher {
self.registered_status_notifier_items_changed(&ctxt).await?;
Watcher::status_notifier_item_registered(&ctxt, item.as_ref()).await?;

self.tasks.spawn({
self.tasks.lock().unwrap().spawn({
let items = self.items.clone();
let ctxt = ctxt.to_owned();
let con = con.to_owned();
Expand Down Expand Up @@ -189,19 +188,46 @@ impl Watcher {
}

/// Attach and run the Watcher (in the background) on a connection.
///
/// If another process already owns the `org.kde.StatusNotifierWatcher` well-known name, a
/// background task is spawned that monitors `NameOwnerChanged` and claims the name as soon
/// as the current owner departs. This avoids the silent state-desync that would otherwise
/// occur if eww queued for the name and later became primary owner while its host-side
/// registrations were still bound to the original owner.
pub async fn attach_to(self, con: &zbus::Connection) -> zbus::Result<()> {
// Clone before moving `self` into the object server so we can still spawn the claim task
// onto the Watcher's JoinSet if needed.
let tasks = self.tasks.clone();

if !con.object_server().at(names::WATCHER_OBJECT, self).await? {
return Err(zbus::Error::Failure(format!(
"Object already exists at {} on this connection -- is StatusNotifierWatcher already running?",
names::WATCHER_OBJECT
)));
}

// not AllowReplacement, not ReplaceExisting, not DoNotQueue
let flags: [zbus::fdo::RequestNameFlags; 0] = [];
// zbus 3.x maps a `RequestNameReply::Exists` response to `Err(NameTaken)`, so both arms
// mean "another process owns the name."
let flags = [zbus::fdo::RequestNameFlags::DoNotQueue];
match con.request_name_with_flags(names::WATCHER_BUS, flags.into_iter().collect()).await {
Ok(zbus::fdo::RequestNameReply::PrimaryOwner) => Ok(()),
Ok(_) | Err(zbus::Error::NameTaken) => Ok(()), // defer to existing
Ok(zbus::fdo::RequestNameReply::PrimaryOwner) | Ok(zbus::fdo::RequestNameReply::AlreadyOwner) => Ok(()),
Err(zbus::Error::NameTaken) => {
log::info!(
"{} is already owned by another process; will claim it when the current owner departs",
names::WATCHER_BUS
);
let con = con.clone();
// Self-terminates once the name is acquired. Lives on the Watcher's JoinSet so
// it's aborted alongside the other background tasks when the Watcher is dropped.
tasks.lock().unwrap().spawn(async move {
if let Err(e) = claim_watcher_name_when_free(&con).await {
log::error!("failed to claim {}: {}", names::WATCHER_BUS, e);
}
});

Ok(())
}
Ok(reply) => unreachable!("unexpected RequestName reply with DoNotQueue: {:?}", reply),
Err(e) => Err(e),
}
}
Expand Down Expand Up @@ -278,6 +304,39 @@ async fn parse_service<'a>(
}
}

/// Wait until the current owner of `org.kde.StatusNotifierWatcher` departs, then claim the name.
///
/// Loops to handle the case where a third party claims the name between when the previous owner
/// departed and when we issued our own `RequestName`.
async fn claim_watcher_name_when_free(con: &zbus::Connection) -> zbus::Result<()> {
let dbus = zbus::fdo::DBusProxy::new(con).await?;
let watcher_bus =
zbus::names::BusName::try_from(names::WATCHER_BUS).expect("WATCHER_BUS is a valid well-known name");
let mut owner_changes = dbus.receive_name_owner_changed_with_args(&[(0, &watcher_bus)]).await?;

loop {
let flags = [zbus::fdo::RequestNameFlags::DoNotQueue];
match con.request_name_with_flags(names::WATCHER_BUS, flags.into_iter().collect()).await {
Ok(zbus::fdo::RequestNameReply::PrimaryOwner) | Ok(zbus::fdo::RequestNameReply::AlreadyOwner) => {
log::info!("acquired {} after previous owner departed", names::WATCHER_BUS);

return Ok(());
}
Err(zbus::Error::NameTaken) => {
// Someone else still owns it; wait for the next departure and retry.
while let Some(sig) = owner_changes.next().await {
let args = sig.args()?;
if args.new_owner().is_none() {
break;
}
}
}
Ok(reply) => unreachable!("unexpected RequestName reply with DoNotQueue: {:?}", reply),
Err(e) => return Err(e),
}
}
}

/// Wait for a DBus service to disappear
async fn wait_for_service_exit(con: &zbus::Connection, service: zbus::names::BusName<'_>) -> zbus::fdo::Result<()> {
let dbus = zbus::fdo::DBusProxy::new(con).await?;
Expand Down
Loading