D-Bus Phase 3/4: upgrade sessiond, services, add StatusNotifierWatcher, consolidate configs

- redbear-sessiond: add Manager.Inhibit (pipe FD), CanPowerOff/CanReboot/
  CanSuspend/CanHibernate/CanHybridSleep/CanSleep (return na), PowerOff/
  Reboot/Suspend stubs, GetSessionByPID, ListUsers, ListSeats,
  ListInhibitors, ActivateSession/LockSession/UnlockSession/TerminateSession
- redbear-sessiond: add Session SetIdleHint, SetLockedHint, SetType,
  Terminate methods; wire PauseDevice/ResumeDevice/Lock/Unlock signal
  emission via SignalEmitter injection; add dynamic device enumeration
  scanning /scheme/drm/card* and /dev/input/event* at startup
- redbear-sessiond: replace infinite pending() with stoppable shutdown
  via tokio watch channel + control socket shutdown command
- redbear-upower: add Changed signal emission with 30s periodic polling
  and power state snapshot comparison
- redbear-notifications: add ActionInvoked signal, expand capabilities
  to body + body-markup + actions
- redbear-polkit, redbear-udisks: replace pending() with stoppable
  shutdown via signal handling + watch channel
- Add redbear-statusnotifierwatcher: new session bus service implementing
  org.freedesktop.StatusNotifierWatcher for KDE system tray
- Add D-Bus activation file for StatusNotifierWatcher
- KWin session.cpp: try LogindSession before NoopSession fallback
- Consolidate config profiles: remove obsolete redbear-desktop, redbear-kde,
  redbear-live-*, redbear-minimal-*, redbear-wayland configs; simplify
  to three supported targets (redbear-full, redbear-mini, redbear-grub)
- Update DBUS-INTEGRATION-PLAN.md and DESKTOP-STACK-CURRENT-STATUS.md
  with Phase 3/4 fragility assessment, KWin readiness matrix, and
  completeness gap analysis
This commit is contained in:
2026-04-25 12:01:25 +01:00
parent 0bd58c912f
commit dc69317ddf
55 changed files with 1535 additions and 1932 deletions
@@ -0,0 +1,12 @@
[package]
name = "redbear-statusnotifierwatcher"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "redbear-statusnotifierwatcher"
path = "src/main.rs"
[dependencies]
zbus = { version = "5", default-features = false, features = ["tokio"] }
tokio = { version = "1", features = ["full"] }
@@ -0,0 +1,168 @@
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use zbus::{
fdo::{self, ObjectManager},
interface,
object_server::SignalEmitter,
connection::Builder as ConnectionBuilder,
zvariant::ObjectPath,
};
const BUS_NAME: &str = "org.freedesktop.StatusNotifierWatcher";
const OBJECT_PATH: &str = "/StatusNotifierWatcher";
/// org.freedesktop.StatusNotifierWatcher D-Bus interface
/// Tracks registered system tray items and hosts for KDE Plasma.
struct StatusNotifierWatcher {
items: Arc<Mutex<HashSet<String>>>,
hosts: Arc<Mutex<HashSet<String>>>,
}
#[interface(name = "org.freedesktop.StatusNotifierWatcher")]
impl StatusNotifierWatcher {
// --- Methods ---
/// Register a status notifier item.
/// The item parameter is either a full object path (e.g., "/org/example/Item")
/// sent by the item itself, or a bus name (e.g., ":1.42" or "org.example.App")
/// sent via the KDE protocol extension.
async fn register_status_notifier_item(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
item: &str,
) -> fdo::Result<()> {
let is_new = {
let mut items = self.items.lock().map_err(|e| {
fdo::Error::Failed(format!("items lock poisoned: {e}"))
})?;
items.insert(item.to_owned())
};
if is_new {
eprintln!("statusnotifierwatcher: item registered: {item}");
let _ = Self::status_notifier_item_registered(&signal_emitter, item).await;
}
Ok(())
}
/// Register a status notifier host (typically the system tray panel).
async fn register_status_notifier_host(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
host: &str,
) -> fdo::Result<()> {
let is_new = {
let mut hosts = self.hosts.lock().map_err(|e| {
fdo::Error::Failed(format!("hosts lock poisoned: {e}"))
})?;
hosts.insert(host.to_owned())
};
if is_new {
eprintln!("statusnotifierwatcher: host registered: {host}");
}
Ok(())
}
// --- Properties ---
/// List of registered status notifier item bus names / paths.
#[zbus(property)]
fn registered_status_notifier_items(&self) -> fdo::Result<Vec<String>> {
let items = self.items.lock().map_err(|e| {
fdo::Error::Failed(format!("items lock poisoned: {e}"))
})?;
Ok(items.iter().cloned().collect())
}
/// Whether at least one status notifier host is registered.
#[zbus(property)]
fn is_status_notifier_host_registered(&self) -> fdo::Result<bool> {
let hosts = self.hosts.lock().map_err(|e| {
fdo::Error::Failed(format!("hosts lock poisoned: {e}"))
})?;
Ok(!hosts.is_empty())
}
/// Protocol version (always 0 per spec).
#[zbus(property)]
fn protocol_version(&self) -> i32 {
0
}
// --- Signals ---
/// Emitted when a new status notifier item is registered.
#[zbus(signal, name = "StatusNotifierItemRegistered")]
async fn status_notifier_item_registered(
signal_emitter: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
/// Emitted when a status notifier item is unregistered.
#[zbus(signal, name = "StatusNotifierItemUnregistered")]
async fn status_notifier_item_unregistered(
signal_emitter: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
}
async fn wait_for_session_bus() {
for _ in 0..30 {
if std::env::var("DBUS_SESSION_BUS_ADDRESS").is_ok() {
return;
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
wait_for_session_bus().await;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
// Signal handler task for clean shutdown
let signal_tx = shutdown_tx.clone();
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = signal_tx.send(true);
});
// Keep original sender alive so receiver doesn't see all-senders-dropped
let _shutdown_guard = shutdown_tx;
let watcher = StatusNotifierWatcher {
items: Arc::new(Mutex::new(HashSet::new())),
hosts: Arc::new(Mutex::new(HashSet::new())),
};
let path: ObjectPath<'_> = OBJECT_PATH.try_into()?;
let connection = ConnectionBuilder::session()?
.name(BUS_NAME)?
.serve_at(path, watcher)?
.build()
.await?;
eprintln!("statusnotifierwatcher: {BUS_NAME} registered on session bus");
// Wait for shutdown signal
let _ = shutdown_rx.changed().await;
eprintln!("statusnotifierwatcher: shutdown signal received, exiting cleanly");
drop(connection);
Ok(())
}