status-area: Use seperate socket-activated daemon for StatusNotifierWatcher, and other fixes (#1270)

This commit is contained in:
Jeremy Soller 2026-02-04 20:14:37 -07:00 committed by GitHub
commit ec930b2a96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 377 additions and 69 deletions

3
.gitignore vendored
View file

@ -24,3 +24,6 @@ debian/*
!debian/links
!debian/rules
!debian/source
cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service
cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service

View file

@ -0,0 +1,7 @@
[Unit]
Description=COSMIC Status Notifier Watcher backend
[Service]
Type=dbus
BusName=com.system76.CosmicStatusNotifierWatcher
ExecStart=@bindir@/cosmic-applet-status-area --status-notifier-watcher

View file

@ -0,0 +1,4 @@
[D-BUS Service]
Name=com.system76.CosmicStatusNotifierWatcher
Exec=@bindir@/cosmic-applet-status-area --status-notifier-watcher
SystemdService=com.system76.CosmicStatusNotifierWatcher.service

View file

@ -0,0 +1,7 @@
[Unit]
Description=COSMIC Status Notifier Watcher backend
[Service]
Type=dbus
BusName=com.system76.CosmicStatusNotifierWatcher
ExecStart=@bindir@/cosmic-applet-status-area --status-notifier-watcher

View file

@ -16,7 +16,10 @@ use cosmic::{
};
use std::collections::BTreeMap;
use crate::{components::status_menu, subscriptions::status_notifier_watcher};
use crate::{
components::status_menu,
subscriptions::{status_notifier_item::StatusNotifierItem, status_notifier_watcher},
};
#[derive(Clone, Debug)]
pub enum Msg {
@ -158,13 +161,7 @@ impl cosmic::Application for App {
});
} else {
if let Some(menu) = self.menus.get(&id) {
let item_proxy = menu.item.item_proxy().clone();
return Task::future(async move {
match item_proxy.activate(0, 0).await {
Ok(_) => cosmic::action::app(Msg::None),
Err(_) => cosmic::action::app(Msg::TogglePopup(id)),
}
});
return activate(id, &menu.item, None);
}
}
Task::none()
@ -290,29 +287,7 @@ impl cosmic::Application for App {
if let Some(id_str) = id.strip_prefix("activate:") {
if let Ok(real_id) = id_str.parse::<usize>() {
if let Some(menu) = self.menus.get(&real_id) {
let item_proxy = menu.item.item_proxy().clone();
let token = token.clone();
let id = real_id;
return Task::future(async move {
if let Some(t) = token {
match item_proxy.provide_xdg_activation_token(t).await {
Ok(_) => {
println!("Token provided successfully to {}", id)
}
Err(e) => eprintln!(
"Failed to provide token to {}: {}",
id, e
),
}
}
match item_proxy.activate(0, 0).await {
Ok(_) => cosmic::action::app(Msg::None),
Err(err) => {
eprintln!("Activate failed: {}", err);
cosmic::action::app(Msg::TogglePopup(id))
}
}
});
return activate(real_id, &menu.item, token.clone());
}
}
return Task::none();
@ -563,6 +538,34 @@ impl cosmic::Application for App {
}
}
fn activate(
id: usize,
item: &StatusNotifierItem,
activation_token: Option<String>,
) -> Task<cosmic::Action<Msg>> {
if item.is_menu() {
return Task::done(cosmic::action::app(Msg::TogglePopup(id)));
}
let item_proxy = item.item_proxy().clone();
Task::future(async move {
if let Some(t) = activation_token {
match item_proxy.provide_xdg_activation_token(t).await {
Ok(_) => {
tracing::debug!("Token provided successfully to {}", id)
}
Err(e) => tracing::error!("Failed to provide token to {}: {}", id, e),
}
}
match item_proxy.activate(0, 0).await {
Ok(_) => cosmic::action::app(Msg::None),
Err(err) => {
tracing::error!("Activate failed: {}", err);
cosmic::action::app(Msg::TogglePopup(id))
}
}
})
}
fn menu_icon_button<'a>(
applet: &'a cosmic::applet::Context,
menu: &'a status_menu::State,

View file

@ -121,14 +121,6 @@ impl State {
let item_proxy = self.item.item_proxy().clone();
let Some(menu_proxy) = self.item.menu_proxy().cloned() else {
tokio::spawn(async move {
let _ = item_proxy.provide_xdg_activation_token(token).await;
if let Err(err) = item_proxy.activate(0, 0).await {
tracing::error!(
"Error activating status notifier item without menu proxy: {err:?}"
);
}
});
return iced::Task::none();
};
tokio::spawn(async move {
@ -242,7 +234,11 @@ fn layout_view(layout: &Layout, expanded: Option<i32>) -> cosmic::Element<'_, Ms
.symbolic(true);
children.push(icon.into());
}
let button = row_button(children).on_press(Msg::Click(i.id(), is_submenu));
let mut button = row_button(children);
if i.enabled() {
button = button.on_press(Msg::Click(i.id(), is_submenu));
}
if is_submenu && is_expanded {
Some(

View file

@ -1,9 +1,23 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{env, process};
mod components;
mod subscriptions;
pub mod status_notifier_watcher;
mod unique_names;
pub fn run() -> cosmic::iced::Result {
components::app::main()
if let Some(arg) = env::args().nth(1) {
if arg == "--status-notifier-watcher" {
status_notifier_watcher::run()
} else {
tracing::error!("Invalid argument `{arg}` for status-area applet`");
process::exit(1);
}
} else {
components::app::main()
}
}

View file

@ -0,0 +1,155 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
//! A seperate DBus socket-activated daemon to serve as the status notifier watcher
//!
//! By socket-activating this daemon, panel configuration changes do not end up terminating
//! the daemon and having different applet instances race to start it.
//!
//! This provides a seperate interface from the standard one, with a single register method, so it
//! can be socket-activated and not conflict with anything else running as a status notifier
//! watcher.
//!
//! The daemon runs as long as as there is at least one client still connected. Which it checks
//! for every `REFRESH_INTERVAL`.
use crate::subscriptions::status_notifier_watcher::server::create_service;
use crate::unique_names::UniqueNames;
use futures::StreamExt;
use std::{collections::HashSet, time::Duration};
use zbus::fdo;
use zbus::message::Header;
const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher";
const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher";
const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// Run daemon
pub fn run() -> cosmic::iced::Result {
if let Err(err) = run_inner() {
eprintln!("Zbus error running status notifier watcher: {}", err);
std::process::exit(1);
}
Ok(())
}
/// Register client with daemon
pub async fn cosmic_register(conn: &zbus::Connection) -> zbus::Result<()> {
let cosmic_watcher = CosmicAppletStatusNotifierWatcherProxy::new(conn).await?;
cosmic_watcher.register_applet().await?;
let mut stream = cosmic_watcher.0.receive_owner_changed().await?;
tokio::spawn(async move {
while let Some(value) = stream.next().await {
if let Some(_unique_name) = value {
/// Register with new owner
let _ = cosmic_watcher.register_applet().await;
}
}
});
Ok(())
}
#[zbus::proxy(
interface = "com.system76.CosmicStatusNotifierWatcher",
default_service = "com.system76.CosmicStatusNotifierWatcher",
default_path = "/CosmicStatusNotifierWatcher"
)]
trait CosmicAppletStatusNotifierWatcher {
async fn register_applet(&self) -> zbus::Result<()>;
}
struct CosmicAppletStatusNotifierWatcher {
applets: HashSet<zbus::names::UniqueName<'static>>,
unique_names: UniqueNames,
}
#[zbus::interface(name = "com.system76.CosmicStatusNotifierWatcher")]
impl CosmicAppletStatusNotifierWatcher {
fn register_applet(&mut self, #[zbus(header)] hdr: Header<'_>) {
if let Some(sender) = hdr.sender() {
if self.unique_names.has_unique_name(sender) {
self.applets.insert(sender.to_owned());
}
}
}
}
impl CosmicAppletStatusNotifierWatcher {
fn has_client(&self) -> bool {
!self.applets.is_empty()
}
/// Purge registered clients that no longer exist on bus
fn refresh(&mut self) {
self.applets
.retain(|n| self.unique_names.has_unique_name(n));
}
}
#[tokio::main]
pub async fn run_inner() -> zbus::Result<()> {
let (running, abort_handle) = futures::future::abortable(std::future::pending::<()>());
let conn = zbus::Connection::session().await?;
create_service(&conn).await?;
let dbus = zbus::fdo::DBusProxy::new(&conn).await?;
conn.object_server()
.at(
OBJECT_PATH,
CosmicAppletStatusNotifierWatcher {
applets: HashSet::new(),
unique_names: UniqueNames::new(&conn).await?,
},
)
.await?;
let interface = conn
.object_server()
.interface::<_, CosmicAppletStatusNotifierWatcher>(OBJECT_PATH)
.await?;
tokio::spawn(refresh_task(interface.clone(), abort_handle.clone()));
let name_lost_stream = dbus.receive_name_lost().await?;
tokio::spawn(name_lost_task(name_lost_stream, abort_handle));
conn.request_name(DBUS_NAME).await?;
let _ = running.await;
Ok(())
}
// Task to terminate daemon with the owned name is lost.
// (If a different instance of this daemon is manually started.)
async fn name_lost_task(
mut name_lost_stream: fdo::NameLostStream,
abort_handle: futures::future::AbortHandle,
) {
while let Some(name_lost) = name_lost_stream.next().await {
let Ok(args) = name_lost.args() else {
return;
};
if args.name == DBUS_NAME {
eprintln!("'{}' name on bus lost. Exiting.", DBUS_NAME);
abort_handle.abort();
return;
}
}
}
async fn refresh_task(
interface: zbus::object_server::InterfaceRef<CosmicAppletStatusNotifierWatcher>,
abort_handle: futures::future::AbortHandle,
) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
// Initial tick, waiting for first client to connect
interval.tick().await;
loop {
interval.tick().await;
let mut watcher = interface.get_mut().await;
if !watcher.has_client() {
// No clients since last refresh; exit
abort_handle.abort();
return;
}
watcher.refresh();
}
}

View file

@ -10,6 +10,7 @@ use zbus::zvariant::{self, OwnedValue};
#[derive(Clone, Debug)]
pub struct StatusNotifierItem {
name: String,
is_menu: bool,
item_proxy: StatusNotifierItemProxy<'static>,
menu_proxy: Option<DBusMenuProxy<'static>>,
}
@ -44,32 +45,25 @@ impl StatusNotifierItem {
.build()
.await?;
// XX: some items will not implement this but have a menu anyway
let is_menu = item_proxy.item_is_menu().await;
let is_menu = item_proxy.item_is_menu().await.unwrap_or(false);
let menu_path = item_proxy.menu().await;
// Why would an item say it has no menu but provide a menu path? Slack does this.
let is_menu = menu_path.is_ok() || is_menu.unwrap_or(false);
if !is_menu {
return Ok(Self {
name,
item_proxy,
menu_proxy: None,
});
}
let menu_path = menu_path?;
let menu_proxy = DBusMenuProxy::builder(connection)
.destination(dest.to_string())?
.path(menu_path)?
.build()
.await?;
let menu_proxy = if let Ok(menu_path) = item_proxy.menu().await {
Some(
DBusMenuProxy::builder(connection)
.destination(dest.to_string())?
.path(menu_path)?
.build()
.await?,
)
} else {
None
};
Ok(Self {
name,
is_menu,
item_proxy,
menu_proxy: Some(menu_proxy),
menu_proxy,
})
}
@ -126,6 +120,11 @@ impl StatusNotifierItem {
)
}
/// Item is only a menu, with no `Activate` action
pub fn is_menu(&self) -> bool {
self.is_menu
}
pub fn menu_proxy(&self) -> Option<&DBusMenuProxy<'static>> {
self.menu_proxy.as_ref()
}

View file

@ -9,7 +9,7 @@ use futures::{StreamExt, stream};
use crate::subscriptions::status_notifier_item::StatusNotifierItem;
mod client;
mod server;
pub(crate) mod server;
#[derive(Clone, Debug)]
pub enum Event {
@ -51,7 +51,9 @@ async fn connect() -> zbus::Result<(zbus::Connection, client::EventStream)> {
let connection = zbus::Connection::session().await?;
// Start `StatusNotifierWatcher` service, if there isn't one running already
server::create_service(&connection).await?;
if let Err(err) = crate::status_notifier_watcher::cosmic_register(&connection).await {
eprintln!("Failed to start status notifier watcher: {}", err);
}
// Connect client and listen for registered/unregistered
let stream = client::watch(&connection).await?;

View file

@ -37,11 +37,15 @@ impl StatusNotifierWatcher {
} else {
service.to_string()
};
Self::status_notifier_item_registered(&ctxt, &service)
.await
.unwrap();
self.items.push((sender.to_owned(), service));
// Ignore duplicate
if !self.items.iter().any(|(a, b)| (a, b) == (sender, &service)) {
Self::status_notifier_item_registered(&ctxt, &service)
.await
.unwrap();
self.items.push((sender.to_owned(), service));
}
}
fn register_status_notifier_host(&self, _service: &str) {

View file

@ -0,0 +1,107 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
// Based on https://github.com/pop-os/cosmic-comp/blob/master/src/dbus/name_owners.rs,
// but only tracking unique names, and using tokio executor.
use futures::StreamExt;
use std::{
collections::HashSet,
future::{Future, poll_fn},
sync::{Arc, Mutex, Weak},
task::{Context, Poll, Waker},
};
use zbus::{
fdo,
names::{BusName, UniqueName},
};
#[derive(Debug)]
struct Inner {
unique_names: HashSet<UniqueName<'static>>,
stream: fdo::NameOwnerChangedStream,
// Waker from `update_task` is stored, so that task will still be woken after
// polling elsewhere.
waker: Waker,
}
impl Drop for Inner {
fn drop(&mut self) {
// Wake `update_task` so it can terminate
self.waker.wake_by_ref();
}
}
impl Inner {
/// Process all events so far on `stream`, and update `unique_names`.
fn update_if_needed(&mut self) {
let mut context = Context::from_waker(&self.waker);
while let Poll::Ready(val) = self.stream.poll_next_unpin(&mut context) {
let val = val.unwrap();
let args = val.args().unwrap();
match args.name {
BusName::Unique(name) => {
if args.new_owner.is_some() {
self.unique_names.insert(name.to_owned());
} else {
self.unique_names.remove(&name.to_owned());
}
}
BusName::WellKnown(_) => {}
}
}
}
}
/// This task polls the steam regularly, to make sure events on the stream aren't just
/// buffered indefinitely.
fn update_task(inner: Weak<Mutex<Inner>>) -> impl Future<Output = ()> {
poll_fn(move |context| {
if let Some(inner) = inner.upgrade() {
let mut inner = inner.lock().unwrap();
inner.waker = context.waker().clone();
inner.update_if_needed();
// Nothing to do now until waker is invoked
Poll::Pending
} else {
// All strong references have been dropped, so task has nothing left to do.
Poll::Ready(())
}
})
}
#[derive(Clone, Debug)]
pub struct UniqueNames(Arc<Mutex<Inner>>);
impl UniqueNames {
pub async fn new(connection: &zbus::Connection) -> zbus::Result<Self> {
let dbus = fdo::DBusProxy::new(connection).await?;
let stream = dbus.receive_name_owner_changed().await?;
let names = dbus.list_names().await?;
let unique_names = names
.iter()
.filter_map(|n| match n.inner() {
BusName::Unique(name) => Some(name.to_owned()),
BusName::WellKnown(_) => None,
})
.collect();
let inner = Arc::new(Mutex::new(Inner {
unique_names,
stream,
waker: Waker::noop().clone(),
}));
tokio::spawn(update_task(Arc::downgrade(&inner)));
Ok(UniqueNames(inner))
}
#[allow(dead_code)]
pub fn has_unique_name(&self, name: &UniqueName<'_>) -> bool {
let mut inner = self.0.lock().unwrap();
inner.update_if_needed();
inner.unique_names.contains(name)
}
}

View file

@ -13,6 +13,7 @@ sharedir := rootdir + prefix + '/share'
iconsdir := sharedir + '/icons/hicolor'
prefixdir := prefix + '/bin'
bindir := rootdir + prefixdir
libdir := rootdir + prefix + '/lib'
default-schema-target := sharedir / 'cosmic'
cosmic-applets-bin := prefixdir / 'cosmic-applets'
@ -57,8 +58,14 @@ _install_button id name: (_install_icons name) (_install_desktop name + '/data/'
_install_metainfo:
install -Dm0644 {{metainfo-src}} {{metainfo-dst}}
_install_status_notifier_watcher:
sed "s|@bindir@|{{prefixdir}}|" cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service
install -Dm0644 cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service {{sharedir}}/dbus-1/services/com.system76.CosmicStatusNotifierWatcher.service
sed "s|@bindir@|{{prefixdir}}|" cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service
install -Dm0644 cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service {{libdir}}/systemd/user/com.system76.CosmicStatusNotifierWatcher.service
# Installs files into the system
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo)
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') (_install_metainfo) (_install_status_notifier_watcher)
# Vendor Cargo dependencies locally
vendor: