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
@@ -151,6 +151,54 @@ impl LoginSession {
Ok(())
}
fn set_idle_hint(&self, idle: bool) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.idle_hint = idle;
}
eprintln!("redbear-sessiond: SetIdleHint({idle}) for session {session_id}");
Ok(())
}
fn set_locked_hint(&self, locked: bool) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.locked_hint = locked;
}
eprintln!("redbear-sessiond: SetLockedHint({locked}) for session {session_id}");
Ok(())
}
fn set_type(&self, session_type: &str) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.session_type = session_type.to_owned();
}
eprintln!("redbear-sessiond: SetType({session_type}) for session {session_id}");
Ok(())
}
fn terminate(&self) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.state = String::from("closing");
}
eprintln!("redbear-sessiond: Terminate requested for session {session_id}");
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "Active")]
fn active(&self) -> bool {
self.runtime().map(|runtime| runtime.active).unwrap_or(true)
@@ -161,9 +209,11 @@ impl LoginSession {
false
}
#[zbus(property(emits_changed_signal = "const"), name = "Type")]
#[zbus(property(emits_changed_signal = "false"), name = "Type")]
fn kind(&self) -> String {
String::from("wayland")
self.runtime()
.map(|r| r.session_type.clone())
.unwrap_or_else(|_| String::from("wayland"))
}
#[zbus(property(emits_changed_signal = "const"), name = "Class")]
@@ -191,7 +241,7 @@ impl LoginSession {
self.runtime().map(|runtime| runtime.session_id).unwrap_or_else(|_| String::from("c1"))
}
#[zbus(property(emits_changed_signal = "const"), name = "State")]
#[zbus(property(emits_changed_signal = "false"), name = "State")]
fn state(&self) -> String {
self.runtime().map(|runtime| runtime.state).unwrap_or_else(|_| String::from("online"))
}
@@ -244,14 +294,14 @@ impl LoginSession {
String::new()
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
#[zbus(property(emits_changed_signal = "false"), name = "IdleHint")]
fn idle_hint(&self) -> bool {
false
self.runtime().map(|r| r.idle_hint).unwrap_or(false)
}
#[zbus(property(emits_changed_signal = "const"), name = "LockedHint")]
#[zbus(property(emits_changed_signal = "false"), name = "LockedHint")]
fn locked_hint(&self) -> bool {
false
self.runtime().map(|r| r.locked_hint).unwrap_or(false)
}
#[zbus(signal, name = "PauseDevice")]
@@ -269,4 +319,108 @@ impl LoginSession {
minor: u32,
fd: Fd<'_>,
) -> zbus::Result<()>;
#[zbus(signal, name = "Lock")]
async fn lock(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
#[zbus(signal, name = "Unlock")]
async fn unlock(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device_map::DeviceMap;
use crate::runtime_state::shared_runtime;
fn test_session() -> LoginSession {
LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
shared_runtime(),
)
}
#[test]
fn set_idle_hint_updates_runtime() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert!(!session.idle_hint());
session.set_idle_hint(true).unwrap();
assert!(session.idle_hint());
let guard = runtime.read().expect("lock");
assert!(guard.idle_hint);
}
#[test]
fn set_locked_hint_updates_runtime() {
let session = test_session();
assert!(!session.locked_hint());
session.set_locked_hint(true).unwrap();
assert!(session.locked_hint());
}
#[test]
fn set_type_updates_runtime() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert_eq!(session.kind(), "wayland");
session.set_type("x11").unwrap();
assert_eq!(session.kind(), "x11");
let guard = runtime.read().expect("lock");
assert_eq!(guard.session_type, "x11");
}
#[test]
fn terminate_sets_state_to_closing() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert_eq!(session.state(), "online");
session.terminate().unwrap();
assert_eq!(session.state(), "closing");
}
#[test]
fn take_control_then_take_device_rejects_duplicate() {
let session = test_session();
session.take_control(false).unwrap();
session.taken_devices().unwrap().insert((226, 0));
let err = session.take_device(226, 0).unwrap_err();
match err {
fdo::Error::Failed(msg) => assert!(msg.contains("already taken")),
other => panic!("expected Failed error, got {other:?}"),
}
}
#[test]
fn release_device_rejects_unknown() {
let session = test_session();
session.take_control(false).unwrap();
let err = session.release_device(226, 99).unwrap_err();
match err {
fdo::Error::Failed(msg) => assert!(msg.contains("was not taken")),
other => panic!("expected Failed error, got {other:?}"),
}
}
}