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.
This commit is contained in:
Ian Douglas Scott 2025-12-09 18:41:37 -08:00
parent fe0e4bf409
commit aa438821b9
9 changed files with 310 additions and 4 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

@ -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

@ -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

@ -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: