From aa438821b96199e01647320a048d0e67837fabc3 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 9 Dec 2025 18:41:37 -0800 Subject: [PATCH] status-area: Seperate daemon for status notifier daemon This allows the applet to be restarted on panel configuration changes without replacing the daemon, or having races between different applet instances trying to run the watcher. Otherwise, this should behave similarly to the existing version. Should fix https://github.com/pop-os/cosmic-panel/issues/284. --- .gitignore | 3 + ...m76.CosmicStatusNotifierWatcher.service.in | 7 + ...m76.CosmicStatusNotifierWatcher.service.in | 4 + ...ktop.impl.portal.desktop.cosmic.service.in | 7 + cosmic-applet-status-area/src/lib.rs | 16 +- .../src/status_notifier_watcher.rs | 155 ++++++++++++++++++ .../status_notifier_watcher/mod.rs | 6 +- cosmic-applet-status-area/src/unique_names.rs | 107 ++++++++++++ justfile | 9 +- 9 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in create mode 100644 cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in create mode 100644 cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in create mode 100644 cosmic-applet-status-area/src/status_notifier_watcher.rs create mode 100644 cosmic-applet-status-area/src/unique_names.rs diff --git a/.gitignore b/.gitignore index 5c0031bd..e178ac1a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in b/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in new file mode 100644 index 00000000..4f2067ee --- /dev/null +++ b/cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in @@ -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 diff --git a/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in b/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in new file mode 100644 index 00000000..0c3345d5 --- /dev/null +++ b/cosmic-applet-status-area/data/dbus-1/com.system76.CosmicStatusNotifierWatcher.service.in @@ -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 diff --git a/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in b/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in new file mode 100644 index 00000000..4f2067ee --- /dev/null +++ b/cosmic-applet-status-area/data/org.freedesktop.impl.portal.desktop.cosmic.service.in @@ -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 diff --git a/cosmic-applet-status-area/src/lib.rs b/cosmic-applet-status-area/src/lib.rs index 371b8aff..17166cbc 100644 --- a/cosmic-applet-status-area/src/lib.rs +++ b/cosmic-applet-status-area/src/lib.rs @@ -1,9 +1,23 @@ // Copyright 2023 System76 // 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() + } } diff --git a/cosmic-applet-status-area/src/status_notifier_watcher.rs b/cosmic-applet-status-area/src/status_notifier_watcher.rs new file mode 100644 index 00000000..1bb75d79 --- /dev/null +++ b/cosmic-applet-status-area/src/status_notifier_watcher.rs @@ -0,0 +1,155 @@ +// Copyright 2023 System76 +// 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>, + 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, + 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(); + } +} diff --git a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs index af0f74ae..9f9c7d14 100644 --- a/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs +++ b/cosmic-applet-status-area/src/subscriptions/status_notifier_watcher/mod.rs @@ -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?; diff --git a/cosmic-applet-status-area/src/unique_names.rs b/cosmic-applet-status-area/src/unique_names.rs new file mode 100644 index 00000000..cab6483e --- /dev/null +++ b/cosmic-applet-status-area/src/unique_names.rs @@ -0,0 +1,107 @@ +// Copyright 2026 System76 +// 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>, + 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>) -> impl Future { + 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>); + +impl UniqueNames { + pub async fn new(connection: &zbus::Connection) -> zbus::Result { + 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) + } +} diff --git a/justfile b/justfile index a1ea651c..6c5033d3 100644 --- a/justfile +++ b/justfile @@ -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: