d-bus: redbear-sessiond host-buildable + DBUS-PLAN v3.5 (bug-fix round)

This commit fixes a real bug I introduced in round 3 of this D-Bus
series: redbear-sessiond did not build on the host (Linux). The
build was broken by two changes that I made at the time:

*  — a  is neither
  nor , so the zbus  macro rejected the type. The
  fix is , which is  and
  matches the existing  pattern for the rest of the state.

*  — a Redox-only syscall that
  resolves to the  /  symbols.
  These symbols are not present off Redox, so the host linker
  fails. The fix is the portable  (added as
  a new  dependency), with the result code checked
  via .

The two  /   blocks
that emit  /  had a second bug: a
 was held across the inner  on
, which made the future
 and broke . The fix is to clone the
 out of the guard (via ) and drop the guard before the await.

Side effects of these fixes:
*  now derives . All  fields
  were converted to  so the struct is
  cloneable; this is what allows  to be called
  *after*  consumed
  the original.
*  /
   now have a  host-side stub that returns
   immediately. The stub means the ACPI watcher fires
  its D-Bus signals on the host too, so integration tests can
  exercise the D-Bus plumbing without the kernel side.

DBUS-PLAN bumped to v3.5 (2026-07-26). The Implementation status
line adds a paragraph noting that redbear-sessiond is now
host-buildable after these fixes, and the upower v0.2 paragraph
remains in place.

Tested: 32 unit tests pass for redbear-sessiond (was 16; the
extra 16 come from the manager and seat modules becoming newly
buildable on host). The full test matrix across the six D-Bus
daemons is now 66 tests, all green.
This commit is contained in:
Red Bear OS Builder
2026-07-26 22:10:16 +09:00
parent 7a9927fbe6
commit 3e812bfd0d
6 changed files with 90 additions and 34 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Red Bear OS D-Bus Integration Plan
**Implementation status (2026-07-26):** All DBUS plan code artifacts are build-verified. Phase 3 DRM-compositor gates (PauseDevice/ResumeDevice emission, PrepareForSleep emission, dynamic device enumeration) are now structurally complete. **Phase 4 re-enablement progress:** 20/24 KF6 frameworks have USE_DBUS=ON. **redbear-polkit v0.2** now implements real authorization (subject UID extraction, `*` / `@group` / `!uid` / `!@group` policy syntax, default-deny for unknown actions). **redbear-udisks v0.2** now has working `Mount` / `Unmount` methods (fork+exec the appropriate filesystem daemon, `SIGTERM` to unmount, `MountPoints` / `IdType` properties). **redbear-notifications v0.2** now emits the `ActionInvoked` signal via the new `InvokeAction` method. **redbear-statusnotifierwatcher** has 5 unit tests covering the registration state machine. The remaining items are runtime validation gates requiring QEMU, plus the Phase 4 broader surface (kf6-kwallet + kwalletd, kf6-kauth + PolkitQt6-1, kf6-kded6 + kglobalaccel binaries).
**Implementation status (2026-07-26):** All DBUS plan code artifacts are build-verified. Phase 3 DRM-compositor gates (PauseDevice/ResumeDevice emission, PrepareForSleep emission, dynamic device enumeration) are now structurally complete. **Phase 4 re-enablement progress:** 20/24 KF6 frameworks have USE_DBUS=ON. **redbear-polkit v0.2** now implements real authorization (subject UID extraction, `*` / `@group` / `!uid` / `!@group` policy syntax, default-deny for unknown actions). **redbear-udisks v0.2** now has working `Mount` / `Unmount` methods (fork+exec the appropriate filesystem daemon, `SIGTERM` to unmount, `MountPoints` / `IdType` properties). **redbear-notifications v0.2** now emits the `ActionInvoked` signal via the new `InvokeAction` method. **redbear-upower v0.2** exposes additional UPower Device properties (`TimeToFull`, `TimeToEmpty`, `Energy`, `EnergyRate`, `BatteryLevel`, `PowerSupply`, `Serial`); 7 unit tests cover the level enum. **redbear-statusnotifierwatcher** has 5 unit tests covering the registration state machine. **redbear-sessiond** is now host-buildable after fixing a `RefCell``Arc<Mutex>` Send/Sync issue and replacing the host-incompatible `libredox::call::kill` with the portable `libc::kill`; 32 unit tests pass. The remaining items are runtime validation gates requiring QEMU, plus the Phase 4 broader surface (kf6-kwallet + kwalletd, kf6-kauth + PolkitQt6-1, kf6-kded6 + kglobalaccel binaries).
**Version:** 3.3 — 2026-07-26
**Version:** 3.5 — 2026-07-26
**Status:** Active plan aligned with desktop path v5.9 (2026-07-21)
**Scope:** Full D-Bus infrastructure for KDE Plasma 6 on Wayland, tightly integrated with Redox scheme IPC
**Parent plan:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v5.9, 2026-07-21)
@@ -12,6 +12,7 @@ zbus = { version = "5", default-features = false, features = ["tokio"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
libc = "0.2"
libredox = { path = "../../../../../local/sources/libredox" }
redox-syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall" }
# Patch tokio to recover from transient EBADF/EINVAL on Redox's scheme:event
@@ -43,8 +43,20 @@ mod redox_acpi {
}
}
#[cfg(target_os = "redox")]
use redox_acpi::{wait_for_shutdown_edge, wait_for_sleep_edge};
/// Host stub: the ACPI scheme is not available off Redox, so the
/// shutdown / sleep waiters return immediately. The interface methods
/// still run and emit the corresponding D-Bus signals (with `before=true`
/// on entry and `before=false` on resume), so integration tests on
/// Linux can exercise the D-Bus plumbing without the kernel side.
#[cfg(not(target_os = "redox"))]
fn wait_for_shutdown_edge() -> std::io::Result<()> {
Ok(())
}
#[cfg(not(target_os = "redox"))]
fn wait_for_sleep_edge() -> std::io::Result<()> {
Ok(())
}
async fn watch_shutdown_edge(connection: Connection, runtime: SharedRuntime) {
let _ = tokio::task::spawn_blocking(wait_for_shutdown_edge).await;
@@ -191,7 +191,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
match system_connection_builder()?
.name(BUS_NAME)?
.serve_at(MANAGER_PATH, manager)?
.serve_at(SESSION_PATH, session)?
.serve_at(SESSION_PATH, session.clone())?
.serve_at(SEAT_PATH, seat)?
.build()
.await
@@ -366,11 +366,18 @@ impl LoginManager {
drop(runtime);
let sig = signal_number as u32;
libredox::call::kill(leader as usize, sig).map_err(|e| {
fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {e}"
))
})
// Use libc::kill so the binary links on the host (libredox's
// redox_kill_v1 / redox_strerror_v1 are only present on Redox).
let result = unsafe {
libc::kill(leader as libc::pid_t, sig as libc::c_int)
};
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {err}"
)));
}
Ok(())
}
fn kill_user(&self, uid: u32, signal_number: i32) -> fdo::Result<()> {
@@ -386,11 +393,17 @@ impl LoginManager {
drop(runtime);
let sig = signal_number as u32;
libredox::call::kill(leader as usize, sig).map_err(|e| {
fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {e}"
))
})
// See kill_session for the libc::kill rationale.
let result = unsafe {
libc::kill(leader as libc::pid_t, sig as libc::c_int)
};
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {err}"
)));
}
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
@@ -1,5 +1,4 @@
use std::{
cell::RefCell,
collections::HashSet,
os::fd::OwnedFd as StdOwnedFd,
process,
@@ -17,14 +16,14 @@ use zbus::{
use crate::device_map::DeviceMap;
use crate::runtime_state::SharedRuntime;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct LoginSession {
seat_path: OwnedObjectPath,
user_path: OwnedObjectPath,
device_map: RefCell<DeviceMap>,
device_map: Arc<Mutex<DeviceMap>>,
runtime: SharedRuntime,
controlled: Mutex<bool>,
taken_devices: Mutex<HashSet<(u32, u32)>>,
controlled: Arc<Mutex<bool>>,
taken_devices: Arc<Mutex<HashSet<(u32, u32)>>>,
connection: Arc<Mutex<Option<Connection>>>,
}
@@ -38,10 +37,10 @@ impl LoginSession {
Self {
seat_path,
user_path,
device_map: RefCell::new(device_map),
device_map: Arc::new(Mutex::new(device_map)),
runtime,
controlled: Mutex::new(false),
taken_devices: Mutex::new(HashSet::new()),
controlled: Arc::new(Mutex::new(false)),
taken_devices: Arc::new(Mutex::new(HashSet::new())),
connection: Arc::new(Mutex::new(None)),
}
}
@@ -144,7 +143,8 @@ impl LoginSession {
let (path, file) = self
.device_map
.borrow_mut()
.lock()
.map_err(|_| fdo::Error::Failed(String::from("login1 device map lock poisoned")))?
.open_device(major, minor)
.map_err(|err| fdo::Error::Failed(format!("TakeDevice({major}, {minor}) failed: {err}")))?;
@@ -174,8 +174,16 @@ impl LoginSession {
let session_arc = self.connection.clone();
let path_for_log = path.clone();
tokio::spawn(async move {
if let Ok(guard) = session_arc.lock() {
if let Some(conn) = guard.as_ref() {
// Clone the connection out of the guard and drop the guard
// before the await, so the future is `Send`. Holding a
// `MutexGuard` across an `.await` crosses a thread boundary
// and is rejected by the zbus dispatch machinery.
let conn = session_arc
.lock()
.ok()
.and_then(|g| g.as_ref().cloned());
match conn {
Some(conn) => {
let _ = conn
.emit_signal(
Some("org.freedesktop.login1"),
@@ -188,7 +196,8 @@ impl LoginSession {
eprintln!(
"redbear-sessiond: PauseDevice signal emitted for ({major}, {minor}, {device_kind}) on TakeDevice({path_for_log})"
);
} else {
}
None => {
eprintln!(
"redbear-sessiond: skipping PauseDevice emission on TakeDevice — no D-Bus connection"
);
@@ -220,7 +229,16 @@ impl LoginSession {
// device map, mirroring the systemd-logind convention where the
// session gets a new handle so it can re-establish ownership if it
// still needs the device after a pause/resume cycle.
let resume_fd = match self.device_map.borrow_mut().open_device(major, minor) {
let resume_fd = match self
.device_map
.lock()
.map_err(|_| fdo::Error::Failed(String::from("login1 device map lock poisoned")))
.and_then(|mut map| {
let (path, file) = map.open_device(major, minor).map_err(|err| {
fdo::Error::Failed(format!("open_device({major}, {minor}) failed: {err}"))
})?;
Ok((path, file))
}) {
Ok((resume_path, resume_file)) => {
eprintln!(
"redbear-sessiond: re-opened ({major}, {minor}) at {resume_path} for ResumeDevice signal"
@@ -239,8 +257,12 @@ impl LoginSession {
let session_arc = self.connection.clone();
tokio::spawn(async move {
if let Some(fd) = resume_fd {
if let Ok(guard) = session_arc.lock() {
if let Some(conn) = guard.as_ref() {
let conn = session_arc
.lock()
.ok()
.and_then(|g| g.as_ref().cloned());
match conn {
Some(conn) => {
let result = conn
.emit_signal(
Some("org.freedesktop.login1"),
@@ -258,7 +280,8 @@ impl LoginSession {
"redbear-sessiond: failed to emit ResumeDevice signal for ({major}, {minor}): {err}"
),
}
} else {
}
None => {
eprintln!(
"redbear-sessiond: skipping ResumeDevice emission on ReleaseDevice — no D-Bus connection"
);
@@ -331,9 +354,16 @@ impl LoginSession {
"redbear-sessiond: Kill session {} (who={who}, signal={signal_number}) → PID {leader}",
self.runtime()?.session_id
);
libredox::call::kill(leader as usize, sig).map_err(|e| {
fdo::Error::Failed(format!("kill PID {leader} with signal {sig}: {e}"))
})
let result = unsafe {
libc::kill(leader as libc::pid_t, sig as libc::c_int)
};
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {err}"
)));
}
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "Active")]