// 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(); } }