diff --git a/local/recipes/libs/zbus/recipe.toml b/local/recipes/libs/zbus/recipe.toml
new file mode 100644
index 00000000..541497b6
--- /dev/null
+++ b/local/recipes/libs/zbus/recipe.toml
@@ -0,0 +1,12 @@
+#TODO: zbus — build-ordering marker. Cargo fetches zbus when redbear-sessiond builds.
+# The cargo template cannot build a library-only crate, so this uses a custom no-op script.
+# Remove if the cookbook gains native Rust library recipe support.
+[source]
+path = "source"
+
+[build]
+template = "custom"
+script = """
+echo "zbus: build-ordering marker — actual crate fetched by downstream Cargo builds"
+mkdir -p "${COOKBOOK_STAGE}/usr"
+"""
diff --git a/local/recipes/libs/zbus/source/Cargo.toml b/local/recipes/libs/zbus/source/Cargo.toml
new file mode 100644
index 00000000..d51d3dbf
--- /dev/null
+++ b/local/recipes/libs/zbus/source/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "zbus"
+version = "5.14.0"
+edition = "2024"
+description = "D-Bus bindings for Rust (pure Rust, no C dependencies)"
+
+[lib]
+name = "zbus"
+path = "src/lib.rs"
+
+[dependencies]
+zbus_names = "4"
+zvariant = "5"
+async-broadcast = "0.7"
+async-executor = "1"
+async-fs = "2"
+async-io = "2"
+async-lock = "3"
+async-task = "4"
+blocking = "1"
+enumflags2 = { version = "0.7", features = ["serde"] }
+event-listener = "5"
+futures-core = "0.3"
+futures-util = "0.3"
+hex = "0.4"
+ordered-stream = "0.2"
+rand = "0.8"
+serde = { version = "1", features = ["derive"] }
+serde_repr = "0.1"
+sha1 = { version = "0.10", features = ["std"] }
+static_assertions = "1"
+tokio = { version = "1", features = ["rt", "net", "macros", "sync", "time"] }
+tracing = "0.1"
+xdg-home = "1"
+
+[features]
+default = ["tokio"]
+tokio = []
diff --git a/local/recipes/libs/zbus/source/src/lib.rs b/local/recipes/libs/zbus/source/src/lib.rs
new file mode 100644
index 00000000..6af4972b
--- /dev/null
+++ b/local/recipes/libs/zbus/source/src/lib.rs
@@ -0,0 +1 @@
+pub struct Connection;
diff --git a/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.Notifications.service b/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.Notifications.service
new file mode 100644
index 00000000..e77b1e31
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/session-services/org.freedesktop.Notifications.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Notifications
+Exec=/usr/bin/redbear-notifications
diff --git a/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kded6.service b/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kded6.service
new file mode 100644
index 00000000..e9d7f6b3
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kded6.service
@@ -0,0 +1,4 @@
+#TODO: kded6 daemon not yet built for Redox — D-Bus activation will fail until it exists
+[D-BUS Service]
+Name=org.kde.kded6
+Exec=/usr/bin/kded6
diff --git a/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kglobalaccel.service b/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kglobalaccel.service
new file mode 100644
index 00000000..7cf0b824
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/session-services/org.kde.kglobalaccel.service
@@ -0,0 +1,4 @@
+#TODO: kglobalaccel daemon not yet built for Redox — D-Bus activation will fail until it exists
+[D-BUS Service]
+Name=org.kde.kglobalaccel
+Exec=/usr/bin/kglobalaccel
diff --git a/local/recipes/system/redbear-dbus-services/files/session.d/org.redbear.session.conf b/local/recipes/system/redbear-dbus-services/files/session.d/org.redbear.session.conf
new file mode 100644
index 00000000..f22097df
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/session.d/org.redbear.session.conf
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PolicyKit1.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PolicyKit1.service
new file mode 100644
index 00000000..16e9b131
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.PolicyKit1.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.PolicyKit1
+Exec=/usr/bin/redbear-polkit
+User=root
+SystemdService=redbear-polkit.service
diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UDisks2.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UDisks2.service
new file mode 100644
index 00000000..5cd67098
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UDisks2.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.UDisks2
+Exec=/usr/bin/redbear-udisks
+User=root
+SystemdService=redbear-udisks.service
diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UPower.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UPower.service
new file mode 100644
index 00000000..6f7eb474
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.UPower.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.UPower
+Exec=/usr/bin/redbear-upower
+User=root
+SystemdService=redbear-upower.service
diff --git a/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.login1.service b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.login1.service
new file mode 100644
index 00000000..6443583b
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system-services/org.freedesktop.login1.service
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=org.freedesktop.login1
+Exec=/usr/bin/redbear-sessiond
+User=root
diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PolicyKit1.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PolicyKit1.conf
new file mode 100644
index 00000000..53529971
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.PolicyKit1.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UDisks2.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UDisks2.conf
new file mode 100644
index 00000000..8591283c
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UDisks2.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UPower.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UPower.conf
new file mode 100644
index 00000000..4f8f5688
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.UPower.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.login1.conf b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.login1.conf
new file mode 100644
index 00000000..a16aab3d
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/files/system.d/org.freedesktop.login1.conf
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/recipe.toml b/local/recipes/system/redbear-dbus-services/recipe.toml
new file mode 100644
index 00000000..94c2fdbe
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/recipe.toml
@@ -0,0 +1,16 @@
+[source]
+path = "files"
+
+[build]
+template = "custom"
+script = """
+mkdir -p "${COOKBOOK_STAGE}/usr/share/dbus-1/system-services"
+mkdir -p "${COOKBOOK_STAGE}/usr/share/dbus-1/session-services"
+mkdir -p "${COOKBOOK_STAGE}/etc/dbus-1/system.d"
+mkdir -p "${COOKBOOK_STAGE}/etc/dbus-1/session.d"
+
+cp -a "${COOKBOOK_SOURCE}/system-services/"* "${COOKBOOK_STAGE}/usr/share/dbus-1/system-services/" 2>/dev/null || true
+cp -a "${COOKBOOK_SOURCE}/session-services/"* "${COOKBOOK_STAGE}/usr/share/dbus-1/session-services/" 2>/dev/null || true
+cp -a "${COOKBOOK_SOURCE}/system.d/"* "${COOKBOOK_STAGE}/etc/dbus-1/system.d/" 2>/dev/null || true
+cp -a "${COOKBOOK_SOURCE}/session.d/"* "${COOKBOOK_STAGE}/etc/dbus-1/session.d/" 2>/dev/null || true
+"""
diff --git a/local/recipes/system/redbear-dbus-services/source/session-services/org.freedesktop.Notifications.service b/local/recipes/system/redbear-dbus-services/source/session-services/org.freedesktop.Notifications.service
new file mode 100644
index 00000000..e77b1e31
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/session-services/org.freedesktop.Notifications.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Notifications
+Exec=/usr/bin/redbear-notifications
diff --git a/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kded6.service b/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kded6.service
new file mode 100644
index 00000000..e9d7f6b3
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kded6.service
@@ -0,0 +1,4 @@
+#TODO: kded6 daemon not yet built for Redox — D-Bus activation will fail until it exists
+[D-BUS Service]
+Name=org.kde.kded6
+Exec=/usr/bin/kded6
diff --git a/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kglobalaccel.service b/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kglobalaccel.service
new file mode 100644
index 00000000..7cf0b824
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/session-services/org.kde.kglobalaccel.service
@@ -0,0 +1,4 @@
+#TODO: kglobalaccel daemon not yet built for Redox — D-Bus activation will fail until it exists
+[D-BUS Service]
+Name=org.kde.kglobalaccel
+Exec=/usr/bin/kglobalaccel
diff --git a/local/recipes/system/redbear-dbus-services/source/session.d/org.redbear.session.conf b/local/recipes/system/redbear-dbus-services/source/session.d/org.redbear.session.conf
new file mode 100644
index 00000000..f22097df
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/session.d/org.redbear.session.conf
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.PolicyKit1.service b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.PolicyKit1.service
new file mode 100644
index 00000000..16e9b131
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.PolicyKit1.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.PolicyKit1
+Exec=/usr/bin/redbear-polkit
+User=root
+SystemdService=redbear-polkit.service
diff --git a/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UDisks2.service b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UDisks2.service
new file mode 100644
index 00000000..5cd67098
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UDisks2.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.UDisks2
+Exec=/usr/bin/redbear-udisks
+User=root
+SystemdService=redbear-udisks.service
diff --git a/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UPower.service b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UPower.service
new file mode 100644
index 00000000..6f7eb474
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.UPower.service
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.UPower
+Exec=/usr/bin/redbear-upower
+User=root
+SystemdService=redbear-upower.service
diff --git a/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.login1.service b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.login1.service
new file mode 100644
index 00000000..6443583b
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system-services/org.freedesktop.login1.service
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=org.freedesktop.login1
+Exec=/usr/bin/redbear-sessiond
+User=root
diff --git a/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.PolicyKit1.conf b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.PolicyKit1.conf
new file mode 100644
index 00000000..53529971
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.PolicyKit1.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UDisks2.conf b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UDisks2.conf
new file mode 100644
index 00000000..8591283c
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UDisks2.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UPower.conf b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UPower.conf
new file mode 100644
index 00000000..4f8f5688
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.UPower.conf
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.login1.conf b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.login1.conf
new file mode 100644
index 00000000..a16aab3d
--- /dev/null
+++ b/local/recipes/system/redbear-dbus-services/source/system.d/org.freedesktop.login1.conf
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/local/recipes/system/redbear-notifications/recipe.toml b/local/recipes/system/redbear-notifications/recipe.toml
new file mode 100644
index 00000000..355688aa
--- /dev/null
+++ b/local/recipes/system/redbear-notifications/recipe.toml
@@ -0,0 +1,10 @@
+#TODO: redbear-notifications — minimal org.freedesktop.Notifications daemon. Logs notifications to stderr until a display server integration exists.
+[source]
+path = "source"
+
+[build]
+template = "cargo"
+dependencies = ["dbus"]
+
+[package.files]
+"/usr/bin/redbear-notifications" = "redbear-notifications"
diff --git a/local/recipes/system/redbear-notifications/source/Cargo.toml b/local/recipes/system/redbear-notifications/source/Cargo.toml
new file mode 100644
index 00000000..7ed7d819
--- /dev/null
+++ b/local/recipes/system/redbear-notifications/source/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "redbear-notifications"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "redbear-notifications"
+path = "src/main.rs"
+
+[dependencies]
+zbus = { version = "5", default-features = false, features = ["tokio"] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/local/recipes/system/redbear-notifications/source/src/main.rs b/local/recipes/system/redbear-notifications/source/src/main.rs
new file mode 100644
index 00000000..ed7ab0a1
--- /dev/null
+++ b/local/recipes/system/redbear-notifications/source/src/main.rs
@@ -0,0 +1,176 @@
+use std::{
+ collections::HashMap,
+ env,
+ error::Error,
+ process,
+ sync::atomic::{AtomicU32, Ordering},
+};
+
+use tokio::runtime::Builder as RuntimeBuilder;
+use zbus::{
+ connection::Builder as ConnectionBuilder,
+ interface,
+ object_server::SignalEmitter,
+ zvariant::Value,
+};
+
+const BUS_NAME: &str = "org.freedesktop.Notifications";
+const OBJECT_PATH: &str = "/org/freedesktop/Notifications";
+
+#[derive(Debug)]
+struct Notifications {
+ next_id: AtomicU32,
+}
+
+impl Notifications {
+ fn new() -> Self {
+ Self {
+ next_id: AtomicU32::new(1),
+ }
+ }
+}
+
+#[interface(name = "org.freedesktop.Notifications")]
+impl Notifications {
+ #[zbus(name = "Notify")]
+ fn notify(
+ &self,
+ app_name: &str,
+ _replaces_id: u32,
+ _app_icon: &str,
+ summary: &str,
+ body: &str,
+ _actions: Vec,
+ _hints: HashMap>,
+ _expire_timeout: i32,
+ ) -> u32 {
+ let id = self.next_id.fetch_add(1, Ordering::Relaxed);
+
+ eprintln!("notification: [{app_name}] {summary}: {body}");
+
+ id
+ }
+
+ #[zbus(name = "CloseNotification")]
+ async fn close_notification(
+ &self,
+ #[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
+ id: u32,
+ ) {
+ eprintln!("notification: closed {id}");
+
+ let _ = Self::notification_closed(&signal_emitter, id, 3).await;
+ }
+
+ #[zbus(name = "GetCapabilities")]
+ fn get_capabilities(&self) -> Vec {
+ vec!["body".to_owned()]
+ }
+
+ #[zbus(name = "GetServerInformation")]
+ fn get_server_information(&self) -> (String, String, String, String) {
+ (
+ String::from("redbear-notifications"),
+ String::from("Red Bear OS"),
+ String::from("0.1.0"),
+ String::from("1.2"),
+ )
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Idle")]
+ fn idle(&self) -> bool {
+ false
+ }
+
+ #[zbus(signal, name = "NotificationClosed")]
+ async fn notification_closed(
+ signal_emitter: &SignalEmitter<'_>,
+ id: u32,
+ reason: u32,
+ ) -> zbus::Result<()>;
+}
+
+enum Command {
+ Run,
+ Help,
+}
+
+fn usage() -> &'static str {
+ "Usage: redbear-notifications [--help]"
+}
+
+fn parse_args() -> Result {
+ let mut args = env::args().skip(1);
+
+ match args.next() {
+ None => Ok(Command::Run),
+ Some(arg) if arg == "--help" || arg == "-h" => {
+ if args.next().is_some() {
+ return Err(String::from("unexpected extra arguments after --help"));
+ }
+
+ Ok(Command::Help)
+ }
+ Some(arg) => Err(format!("unrecognized argument '{arg}'")),
+ }
+}
+
+#[cfg(unix)]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ use tokio::signal::unix::{SignalKind, signal};
+
+ let mut terminate = signal(SignalKind::terminate())?;
+
+ tokio::select! {
+ _ = terminate.recv() => Ok(()),
+ _ = tokio::signal::ctrl_c() => Ok(()),
+ }
+}
+
+#[cfg(not(unix))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ tokio::signal::ctrl_c().await?;
+ Ok(())
+}
+
+async fn run_daemon() -> Result<(), Box> {
+ let _connection = ConnectionBuilder::session()?
+ .name(BUS_NAME)?
+ .serve_at(OBJECT_PATH, Notifications::new())?
+ .build()
+ .await?;
+
+ eprintln!("redbear-notifications: registered {BUS_NAME} on the session bus");
+
+ wait_for_shutdown().await?;
+ eprintln!("redbear-notifications: received shutdown signal, exiting cleanly");
+
+ Ok(())
+}
+
+fn main() {
+ match parse_args() {
+ Ok(Command::Help) => {
+ println!("{}", usage());
+ }
+ Ok(Command::Run) => {
+ let runtime = match RuntimeBuilder::new_multi_thread().enable_all().build() {
+ Ok(runtime) => runtime,
+ Err(err) => {
+ eprintln!("redbear-notifications: failed to create tokio runtime: {err}");
+ process::exit(1);
+ }
+ };
+
+ if let Err(err) = runtime.block_on(run_daemon()) {
+ eprintln!("redbear-notifications: fatal error: {err}");
+ process::exit(1);
+ }
+ }
+ Err(err) => {
+ eprintln!("redbear-notifications: {err}");
+ eprintln!("{}", usage());
+ process::exit(1);
+ }
+ }
+}
diff --git a/local/recipes/system/redbear-polkit/recipe.toml b/local/recipes/system/redbear-polkit/recipe.toml
new file mode 100644
index 00000000..5968ac6a
--- /dev/null
+++ b/local/recipes/system/redbear-polkit/recipe.toml
@@ -0,0 +1,10 @@
+#TODO: redbear-polkit — minimal org.freedesktop.PolicyKit1 daemon. Always-permit authorization for at-console users.
+[source]
+path = "source"
+
+[build]
+template = "cargo"
+dependencies = ["dbus"]
+
+[package.files]
+"/usr/bin/redbear-polkit" = "redbear-polkit"
diff --git a/local/recipes/system/redbear-polkit/source/Cargo.toml b/local/recipes/system/redbear-polkit/source/Cargo.toml
new file mode 100644
index 00000000..73086a83
--- /dev/null
+++ b/local/recipes/system/redbear-polkit/source/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "redbear-polkit"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "redbear-polkit"
+path = "src/main.rs"
+
+[dependencies]
+zbus = { version = "5", default-features = false, features = ["tokio"] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/local/recipes/system/redbear-polkit/source/src/main.rs b/local/recipes/system/redbear-polkit/source/src/main.rs
new file mode 100644
index 00000000..8fc9b950
--- /dev/null
+++ b/local/recipes/system/redbear-polkit/source/src/main.rs
@@ -0,0 +1,183 @@
+use std::{collections::HashMap, env, error::Error, process};
+
+use tokio::runtime::Builder as RuntimeBuilder;
+use zbus::{
+ Address,
+ connection::Builder as ConnectionBuilder,
+ interface,
+ zvariant::{ObjectPath, OwnedObjectPath},
+};
+
+const BUS_NAME: &str = "org.freedesktop.PolicyKit1";
+const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
+
+type AuthorizationDetails = HashMap;
+type EnumeratedAction = (
+ String,
+ String,
+ String,
+ String,
+ String,
+ u32,
+ AuthorizationDetails,
+);
+
+#[derive(Debug, Default)]
+struct PolicyKitAuthority;
+
+enum Command {
+ Run,
+ Help,
+}
+
+fn usage() -> &'static str {
+ "Usage: redbear-polkit [--help]"
+}
+
+fn parse_args() -> Result {
+ let mut args = env::args().skip(1);
+
+ match args.next() {
+ None => Ok(Command::Run),
+ Some(arg) if arg == "--help" || arg == "-h" => {
+ if args.next().is_some() {
+ return Err(String::from("unexpected extra arguments after --help"));
+ }
+
+ Ok(Command::Help)
+ }
+ Some(arg) => Err(format!("unrecognized argument '{arg}'")),
+ }
+}
+
+fn parse_object_path(path: &str) -> Result> {
+ Ok(OwnedObjectPath::try_from(path.to_owned())?)
+}
+
+fn system_connection_builder() -> Result, Box> {
+ if let Ok(address) = env::var("DBUS_STARTER_ADDRESS") {
+ Ok(ConnectionBuilder::address(Address::try_from(address.as_str())?)?)
+ } else {
+ Ok(ConnectionBuilder::system()?)
+ }
+}
+
+#[cfg(all(unix, not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ use tokio::signal::unix::{SignalKind, signal};
+
+ let mut terminate = signal(SignalKind::terminate())?;
+
+ tokio::select! {
+ _ = terminate.recv() => Ok(()),
+ _ = tokio::signal::ctrl_c() => Ok(()),
+ }
+}
+
+#[cfg(target_os = "redox")]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ std::future::pending::<()>().await;
+ #[allow(unreachable_code)]
+ Ok(())
+}
+
+#[cfg(all(not(unix), not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ tokio::signal::ctrl_c().await?;
+ Ok(())
+}
+
+#[interface(name = "org.freedesktop.PolicyKit1.Authority")]
+impl PolicyKitAuthority {
+ #[zbus(name = "CheckAuthorization")]
+ fn check_authorization(
+ &self,
+ _action_id: &str,
+ _details: AuthorizationDetails,
+ _flags: u32,
+ _cancellation_id: &str,
+ ) -> (bool, bool, AuthorizationDetails) {
+ (true, false, AuthorizationDetails::new())
+ }
+
+ #[zbus(name = "RegisterAuthenticationAgent")]
+ fn register_authentication_agent(
+ &self,
+ _session: (&str, ObjectPath<'_>),
+ _locale: &str,
+ _object_path: &str,
+ ) {
+ }
+
+ #[zbus(name = "UnregisterAuthenticationAgent")]
+ fn unregister_authentication_agent(
+ &self,
+ _session: (&str, ObjectPath<'_>),
+ _object_path: &str,
+ ) {
+ }
+
+ #[zbus(name = "EnumerateActions")]
+ fn enumerate_actions(&self, _locale: &str) -> Vec {
+ Vec::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "BackendName")]
+ fn backend_name(&self) -> String {
+ String::from("redbear-permit-all")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "BackendVersion")]
+ fn backend_version(&self) -> String {
+ String::from("0.1.0")
+ }
+}
+
+async fn run_daemon() -> Result<(), Box> {
+ eprintln!("redbear-polkit: startup begin");
+ let _authority_path = parse_object_path(AUTHORITY_PATH)?;
+ eprintln!("redbear-polkit: object paths parsed");
+
+ eprintln!("redbear-polkit: starter address={:?}", env::var("DBUS_STARTER_ADDRESS").ok());
+ eprintln!("redbear-polkit: building D-Bus connection");
+ let connection = system_connection_builder()?
+ .name(BUS_NAME)?
+ .serve_at(AUTHORITY_PATH, PolicyKitAuthority)?
+ .build()
+ .await?;
+
+ eprintln!("redbear-polkit: registered {BUS_NAME} on the system bus");
+
+ wait_for_shutdown().await?;
+ drop(connection);
+ eprintln!("redbear-polkit: received shutdown signal, exiting cleanly");
+
+ Ok(())
+}
+
+fn main() {
+ match parse_args() {
+ Ok(Command::Help) => {
+ println!("{}", usage());
+ }
+ Ok(Command::Run) => {
+ let runtime = match RuntimeBuilder::new_multi_thread().enable_all().build() {
+ Ok(runtime) => runtime,
+ Err(err) => {
+ eprintln!("redbear-polkit: failed to create tokio runtime: {err}");
+ process::exit(1);
+ }
+ };
+
+ if let Err(err) = runtime.block_on(run_daemon()) {
+ eprintln!("redbear-polkit: fatal error: {err}");
+ process::exit(1);
+ }
+ }
+ Err(err) => {
+ eprintln!("redbear-polkit: {err}");
+ eprintln!("{}", usage());
+ process::exit(1);
+ }
+ }
+}
diff --git a/local/recipes/system/redbear-sessiond/recipe.toml b/local/recipes/system/redbear-sessiond/recipe.toml
new file mode 100644
index 00000000..241b9fb6
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/recipe.toml
@@ -0,0 +1,9 @@
+[source]
+path = "source"
+
+[build]
+template = "cargo"
+dependencies = ["dbus"]
+
+[package.files]
+"/usr/bin/redbear-sessiond" = "redbear-sessiond"
diff --git a/local/recipes/system/redbear-sessiond/source/Cargo.toml b/local/recipes/system/redbear-sessiond/source/Cargo.toml
new file mode 100644
index 00000000..949c9ecc
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "redbear-sessiond"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "redbear-sessiond"
+path = "src/main.rs"
+
+[dependencies]
+zbus = { version = "5", default-features = false, features = ["tokio"] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
+libredox = "0.1"
+redox-syscall = { package = "redox_syscall", version = "0.7" }
diff --git a/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs
new file mode 100644
index 00000000..fd978d50
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs
@@ -0,0 +1,67 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::time::Duration;
+use zbus::Connection;
+
+static SLEEP_ACTIVE: AtomicBool = AtomicBool::new(false);
+static SHUTDOWN_FIRED: AtomicBool = AtomicBool::new(false);
+
+const ACPI_SLEEP_PATH: &str = "/scheme/acpi/sleep";
+const ACPI_SHUTDOWN_PATH: &str = "/scheme/acpi/shutdown";
+const POLL_INTERVAL: Duration = Duration::from_secs(5);
+
+fn read_acpi_flag(path: &str) -> bool {
+ match std::fs::read_to_string(path) {
+ Ok(content) => {
+ let trimmed = content.trim().to_lowercase();
+ !trimmed.is_empty() && trimmed != "0"
+ }
+ Err(_) => false,
+ }
+}
+
+pub async fn watch_and_emit(connection: Connection) {
+ loop {
+ tokio::time::sleep(POLL_INTERVAL).await;
+
+ let sleep_now = tokio::task::spawn_blocking(|| read_acpi_flag(ACPI_SLEEP_PATH))
+ .await
+ .unwrap_or(false);
+
+ let was_sleeping = SLEEP_ACTIVE.load(Ordering::Relaxed);
+
+ if sleep_now && !was_sleeping {
+ SLEEP_ACTIVE.store(true, Ordering::Relaxed);
+ let _ = connection.emit_signal(
+ None::<&str>,
+ "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager",
+ "PrepareForSleep",
+ &true,
+ ).await;
+ } else if !sleep_now && was_sleeping {
+ SLEEP_ACTIVE.store(false, Ordering::Relaxed);
+ let _ = connection.emit_signal(
+ None::<&str>,
+ "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager",
+ "PrepareForSleep",
+ &false,
+ ).await;
+ }
+
+ let shutdown_now = tokio::task::spawn_blocking(|| read_acpi_flag(ACPI_SHUTDOWN_PATH))
+ .await
+ .unwrap_or(false);
+
+ if shutdown_now && !SHUTDOWN_FIRED.load(Ordering::Relaxed) {
+ SHUTDOWN_FIRED.store(true, Ordering::Relaxed);
+ let _ = connection.emit_signal(
+ None::<&str>,
+ "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager",
+ "PrepareForShutdown",
+ &true,
+ ).await;
+ }
+ }
+}
diff --git a/local/recipes/system/redbear-sessiond/source/src/device_map.rs b/local/recipes/system/redbear-sessiond/source/src/device_map.rs
new file mode 100644
index 00000000..01a8019c
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/device_map.rs
@@ -0,0 +1,47 @@
+use std::{collections::HashMap, fs::File, io};
+
+#[derive(Clone, Debug)]
+pub struct DeviceMap {
+ static_paths: HashMap<(u32, u32), String>,
+}
+
+impl DeviceMap {
+ pub fn new() -> Self {
+ let static_paths = HashMap::from([
+ ((226, 0), String::from("/scheme/drm/card0")),
+ ((226, 1), String::from("/scheme/drm/card1")),
+ ((13, 64), String::from("/dev/input/event0")),
+ ((13, 65), String::from("/dev/input/event1")),
+ ((13, 66), String::from("/dev/input/event2")),
+ ((13, 67), String::from("/dev/input/event3")),
+ ((29, 0), String::from("/dev/fb0")),
+ ((1, 1), String::from("/scheme/null")),
+ ((1, 5), String::from("/scheme/zero")),
+ ((1, 8), String::from("/scheme/rand")),
+ ]);
+
+ Self { static_paths }
+ }
+
+ pub fn resolve(&self, major: u32, minor: u32) -> Option {
+ if let Some(path) = self.static_paths.get(&(major, minor)) {
+ return Some(path.clone());
+ }
+
+ match (major, minor) {
+ (13, minor) if minor >= 68 => Some(format!("/dev/input/event{}", minor - 64)),
+ _ => None,
+ }
+ }
+
+ pub fn open_device(&self, major: u32, minor: u32) -> io::Result {
+ let Some(path) = self.resolve(major, minor) else {
+ return Err(io::Error::new(
+ io::ErrorKind::NotFound,
+ format!("no Red Bear device mapping for major={major}, minor={minor}"),
+ ));
+ };
+
+ File::open(path)
+ }
+}
diff --git a/local/recipes/system/redbear-sessiond/source/src/main.rs b/local/recipes/system/redbear-sessiond/source/src/main.rs
new file mode 100644
index 00000000..116adf54
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/main.rs
@@ -0,0 +1,148 @@
+mod acpi_watcher;
+mod device_map;
+mod manager;
+mod seat;
+mod session;
+
+use std::{
+ env,
+ error::Error,
+ process,
+};
+
+use device_map::DeviceMap;
+use manager::LoginManager;
+use seat::LoginSeat;
+use session::LoginSession;
+use tokio::runtime::Builder as RuntimeBuilder;
+use zbus::{
+ Address,
+ connection::Builder as ConnectionBuilder,
+ zvariant::OwnedObjectPath,
+};
+
+const BUS_NAME: &str = "org.freedesktop.login1";
+const MANAGER_PATH: &str = "/org/freedesktop/login1";
+const SESSION_PATH: &str = "/org/freedesktop/login1/session/c1";
+const SEAT_PATH: &str = "/org/freedesktop/login1/seat/seat0";
+const USER_PATH: &str = "/org/freedesktop/login1/user/0";
+
+enum Command {
+ Run,
+ Help,
+}
+
+fn usage() -> &'static str {
+ "Usage: redbear-sessiond [--help]"
+}
+
+fn parse_args() -> Result {
+ let mut args = env::args().skip(1);
+
+ match args.next() {
+ None => Ok(Command::Run),
+ Some(arg) if arg == "--help" || arg == "-h" => {
+ if args.next().is_some() {
+ return Err(String::from("unexpected extra arguments after --help"));
+ }
+
+ Ok(Command::Help)
+ }
+ Some(arg) => Err(format!("unrecognized argument '{arg}'")),
+ }
+}
+
+fn parse_object_path(path: &str) -> Result> {
+ Ok(OwnedObjectPath::try_from(path.to_owned())?)
+}
+
+fn system_connection_builder() -> Result, Box> {
+ if let Ok(address) = env::var("DBUS_STARTER_ADDRESS") {
+ Ok(ConnectionBuilder::address(Address::try_from(address.as_str())?)?)
+ } else {
+ Ok(ConnectionBuilder::system()?)
+ }
+}
+
+#[cfg(all(unix, not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ use tokio::signal::unix::{SignalKind, signal};
+
+ let mut terminate = signal(SignalKind::terminate())?;
+
+ tokio::select! {
+ _ = terminate.recv() => Ok(()),
+ _ = tokio::signal::ctrl_c() => Ok(()),
+ }
+}
+
+#[cfg(target_os = "redox")]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ std::future::pending::<()>().await;
+ #[allow(unreachable_code)]
+ Ok(())
+}
+
+#[cfg(all(not(unix), not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ tokio::signal::ctrl_c().await?;
+ Ok(())
+}
+
+async fn run_daemon() -> Result<(), Box> {
+ eprintln!("redbear-sessiond: startup begin");
+ let session_path = parse_object_path(SESSION_PATH)?;
+ let seat_path = parse_object_path(SEAT_PATH)?;
+ let user_path = parse_object_path(USER_PATH)?;
+ eprintln!("redbear-sessiond: object paths parsed");
+
+ let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new());
+ let seat = LoginSeat::new(session_path.clone());
+ let manager = LoginManager::new(session_path, seat_path);
+
+ eprintln!("redbear-sessiond: starter address={:?}", env::var("DBUS_STARTER_ADDRESS").ok());
+ eprintln!("redbear-sessiond: building D-Bus connection");
+ let connection = system_connection_builder()?
+ .name(BUS_NAME)?
+ .serve_at(MANAGER_PATH, manager)?
+ .serve_at(SESSION_PATH, session)?
+ .serve_at(SEAT_PATH, seat)?
+ .build()
+ .await?;
+
+ eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus");
+
+ tokio::spawn(acpi_watcher::watch_and_emit(connection.clone()));
+
+ wait_for_shutdown().await?;
+ eprintln!("redbear-sessiond: received shutdown signal, exiting cleanly");
+
+ Ok(())
+}
+
+fn main() {
+ match parse_args() {
+ Ok(Command::Help) => {
+ println!("{}", usage());
+ }
+ Ok(Command::Run) => {
+ let runtime = match RuntimeBuilder::new_multi_thread().enable_all().build() {
+ Ok(runtime) => runtime,
+ Err(err) => {
+ eprintln!("redbear-sessiond: failed to create tokio runtime: {err}");
+ process::exit(1);
+ }
+ };
+
+ if let Err(err) = runtime.block_on(run_daemon()) {
+ eprintln!("redbear-sessiond: fatal error: {err}");
+ process::exit(1);
+ }
+ }
+ Err(err) => {
+ eprintln!("redbear-sessiond: {err}");
+ eprintln!("{}", usage());
+ process::exit(1);
+ }
+ }
+}
diff --git a/local/recipes/system/redbear-sessiond/source/src/manager.rs b/local/recipes/system/redbear-sessiond/source/src/manager.rs
new file mode 100644
index 00000000..a00c1a7a
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/manager.rs
@@ -0,0 +1,113 @@
+use zbus::{
+ fdo,
+ interface,
+ object_server::SignalEmitter,
+ zvariant::OwnedObjectPath,
+};
+
+#[derive(Clone, Debug)]
+pub struct LoginManager {
+ session_id: String,
+ session_path: OwnedObjectPath,
+ seat_id: String,
+ seat_path: OwnedObjectPath,
+}
+
+impl LoginManager {
+ pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath) -> Self {
+ Self {
+ session_id: String::from("c1"),
+ session_path,
+ seat_id: String::from("seat0"),
+ seat_path,
+ }
+ }
+}
+
+#[interface(name = "org.freedesktop.login1.Manager")]
+impl LoginManager {
+ fn get_session(&self, id: &str) -> fdo::Result {
+ if id == self.session_id {
+ return Ok(self.session_path.clone());
+ }
+
+ Err(fdo::Error::Failed(format!("unknown login1 session '{id}'")))
+ }
+
+ fn list_sessions(&self) -> fdo::Result> {
+ Ok(vec![(
+ self.session_id.clone(),
+ 0,
+ String::from("root"),
+ self.seat_id.clone(),
+ self.session_path.clone(),
+ )])
+ }
+
+ fn get_seat(&self, id: &str) -> fdo::Result {
+ if id == self.seat_id {
+ return Ok(self.seat_path.clone());
+ }
+
+ Err(fdo::Error::Failed(format!("unknown login1 seat '{id}'")))
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
+ fn idle_hint(&self) -> bool {
+ false
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IdleSinceHint")]
+ fn idle_since_hint(&self) -> u64 {
+ 0
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IdleSinceHintMonotonic")]
+ fn idle_since_hint_monotonic(&self) -> u64 {
+ 0
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "BlockInhibited")]
+ fn block_inhibited(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "DelayInhibited")]
+ fn delay_inhibited(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "InhibitDelayMaxUSec")]
+ fn inhibit_delay_max_usec(&self) -> u64 {
+ 0
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "HandleLidSwitch")]
+ fn handle_lid_switch(&self) -> String {
+ String::from("ignore")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "HandlePowerKey")]
+ fn handle_power_key(&self) -> String {
+ String::from("poweroff")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "PreparingForSleep")]
+ fn preparing_for_sleep(&self) -> bool {
+ false
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")]
+ fn preparing_for_shutdown(&self) -> bool {
+ false
+ }
+
+ #[zbus(signal, name = "PrepareForSleep")]
+ async fn prepare_for_sleep(signal_emitter: &SignalEmitter<'_>, before: bool) -> zbus::Result<()>;
+
+ #[zbus(signal, name = "PrepareForShutdown")]
+ async fn prepare_for_shutdown(
+ signal_emitter: &SignalEmitter<'_>,
+ before: bool,
+ ) -> zbus::Result<()>;
+}
diff --git a/local/recipes/system/redbear-sessiond/source/src/seat.rs b/local/recipes/system/redbear-sessiond/source/src/seat.rs
new file mode 100644
index 00000000..b9e4e68a
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/seat.rs
@@ -0,0 +1,71 @@
+use std::sync::Mutex;
+
+use zbus::{fdo, interface, zvariant::OwnedObjectPath};
+
+#[derive(Debug)]
+pub struct LoginSeat {
+ id: String,
+ session_id: String,
+ session_path: OwnedObjectPath,
+ last_requested_vt: Mutex,
+}
+
+impl LoginSeat {
+ pub fn new(session_path: OwnedObjectPath) -> Self {
+ Self {
+ id: String::from("seat0"),
+ session_id: String::from("c1"),
+ session_path,
+ last_requested_vt: Mutex::new(1),
+ }
+ }
+
+ fn last_requested_vt(&self) -> fdo::Result> {
+ self.last_requested_vt
+ .lock()
+ .map_err(|_| fdo::Error::Failed(String::from("seat VT state is poisoned")))
+ }
+}
+
+#[interface(name = "org.freedesktop.login1.Seat")]
+impl LoginSeat {
+ fn switch_to(&mut self, vt: u32) -> fdo::Result<()> {
+ let mut last_requested_vt = self.last_requested_vt()?;
+ *last_requested_vt = vt;
+ eprintln!(
+ "redbear-sessiond: SwitchTo requested for seat {} -> vt {vt} (delegated to inputd -A externally)",
+ self.id
+ );
+ Ok(())
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Id")]
+ fn id(&self) -> String {
+ self.id.clone()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")]
+ fn active_session(&self) -> (String, OwnedObjectPath) {
+ (self.session_id.clone(), self.session_path.clone())
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Sessions")]
+ fn sessions(&self) -> Vec<(String, OwnedObjectPath)> {
+ vec![(self.session_id.clone(), self.session_path.clone())]
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "CanGraphical")]
+ fn can_graphical(&self) -> bool {
+ true
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "CanTTY")]
+ fn can_tty(&self) -> bool {
+ true
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
+ fn idle_hint(&self) -> bool {
+ false
+ }
+}
diff --git a/local/recipes/system/redbear-sessiond/source/src/session.rs b/local/recipes/system/redbear-sessiond/source/src/session.rs
new file mode 100644
index 00000000..a1598d1d
--- /dev/null
+++ b/local/recipes/system/redbear-sessiond/source/src/session.rs
@@ -0,0 +1,232 @@
+use std::{
+ collections::HashSet,
+ os::fd::OwnedFd as StdOwnedFd,
+ process,
+ sync::Mutex,
+};
+
+use zbus::{
+ fdo,
+ interface,
+ object_server::SignalEmitter,
+ zvariant::{Fd, OwnedFd, OwnedObjectPath},
+};
+
+use crate::device_map::DeviceMap;
+
+#[derive(Debug)]
+pub struct LoginSession {
+ id: String,
+ seat_id: String,
+ seat_path: OwnedObjectPath,
+ user_uid: u32,
+ user_path: OwnedObjectPath,
+ leader: u32,
+ device_map: DeviceMap,
+ controlled: Mutex,
+ taken_devices: Mutex>,
+}
+
+impl LoginSession {
+ pub fn new(
+ seat_path: OwnedObjectPath,
+ user_path: OwnedObjectPath,
+ device_map: DeviceMap,
+ ) -> Self {
+ Self {
+ id: String::from("c1"),
+ seat_id: String::from("seat0"),
+ seat_path,
+ user_uid: 0,
+ user_path,
+ leader: process::id(),
+ device_map,
+ controlled: Mutex::new(false),
+ taken_devices: Mutex::new(HashSet::new()),
+ }
+ }
+
+ fn control_state(&self) -> fdo::Result> {
+ self.controlled
+ .lock()
+ .map_err(|_| fdo::Error::Failed(String::from("login1 control state is poisoned")))
+ }
+
+ fn taken_devices(&self) -> fdo::Result>> {
+ self.taken_devices
+ .lock()
+ .map_err(|_| fdo::Error::Failed(String::from("login1 device state is poisoned")))
+ }
+}
+
+#[interface(name = "org.freedesktop.login1.Session")]
+impl LoginSession {
+ fn activate(&self) -> fdo::Result<()> {
+ eprintln!("redbear-sessiond: Activate requested for session {}", self.id);
+ Ok(())
+ }
+
+ fn take_control(&self, force: bool) -> fdo::Result<()> {
+ let mut controlled = self.control_state()?;
+ *controlled = true;
+ eprintln!(
+ "redbear-sessiond: TakeControl requested for session {} (force={force})",
+ self.id
+ );
+ Ok(())
+ }
+
+ fn release_control(&self) -> fdo::Result<()> {
+ let mut controlled = self.control_state()?;
+ *controlled = false;
+ eprintln!("redbear-sessiond: ReleaseControl requested for session {}", self.id);
+ Ok(())
+ }
+
+ fn take_device(&self, major: u32, minor: u32) -> fdo::Result {
+ let file = self
+ .device_map
+ .open_device(major, minor)
+ .map_err(|err| fdo::Error::Failed(format!("TakeDevice({major}, {minor}) failed: {err}")))?;
+
+ let mut taken_devices = self.taken_devices()?;
+ taken_devices.insert((major, minor));
+
+ let owned_fd: StdOwnedFd = file.into();
+ eprintln!(
+ "redbear-sessiond: TakeDevice granted for session {} -> ({major}, {minor})",
+ self.id
+ );
+
+ Ok(OwnedFd::from(owned_fd))
+ }
+
+ fn release_device(&self, major: u32, minor: u32) -> fdo::Result<()> {
+ let mut taken_devices = self.taken_devices()?;
+ taken_devices.remove(&(major, minor));
+ eprintln!(
+ "redbear-sessiond: ReleaseDevice requested for session {} -> ({major}, {minor})",
+ self.id
+ );
+ Ok(())
+ }
+
+ fn pause_device_complete(&self, major: u32, minor: u32) -> fdo::Result<()> {
+ eprintln!(
+ "redbear-sessiond: PauseDeviceComplete received for session {} -> ({major}, {minor})",
+ self.id
+ );
+ Ok(())
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Active")]
+ fn active(&self) -> bool {
+ true
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Remote")]
+ fn remote(&self) -> bool {
+ false
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Type")]
+ fn kind(&self) -> String {
+ String::from("wayland")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Class")]
+ fn class(&self) -> String {
+ String::from("user")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Service")]
+ fn service(&self) -> String {
+ String::from("redbear")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Desktop")]
+ fn desktop(&self) -> String {
+ String::from("KDE")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Display")]
+ fn display(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Id")]
+ fn id(&self) -> String {
+ self.id.clone()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "State")]
+ fn state(&self) -> String {
+ String::from("online")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Seat")]
+ fn seat(&self) -> (String, OwnedObjectPath) {
+ (self.seat_id.clone(), self.seat_path.clone())
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "User")]
+ fn user(&self) -> (u32, OwnedObjectPath) {
+ (self.user_uid, self.user_path.clone())
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "VTNr")]
+ fn vt_nr(&self) -> u32 {
+ 1
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Leader")]
+ fn leader(&self) -> u32 {
+ self.leader
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Audit")]
+ fn audit(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "TTY")]
+ fn tty(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "RemoteUser")]
+ fn remote_user(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "RemoteHost")]
+ fn remote_host(&self) -> String {
+ String::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
+ fn idle_hint(&self) -> bool {
+ false
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "LockedHint")]
+ fn locked_hint(&self) -> bool {
+ false
+ }
+
+ #[zbus(signal, name = "PauseDevice")]
+ async fn pause_device(
+ signal_emitter: &SignalEmitter<'_>,
+ major: u32,
+ minor: u32,
+ kind: String,
+ ) -> zbus::Result<()>;
+
+ #[zbus(signal, name = "ResumeDevice")]
+ async fn resume_device(
+ signal_emitter: &SignalEmitter<'_>,
+ major: u32,
+ minor: u32,
+ fd: Fd<'_>,
+ ) -> zbus::Result<()>;
+}
diff --git a/local/recipes/system/redbear-udisks/recipe.toml b/local/recipes/system/redbear-udisks/recipe.toml
new file mode 100644
index 00000000..d93b49ef
--- /dev/null
+++ b/local/recipes/system/redbear-udisks/recipe.toml
@@ -0,0 +1,10 @@
+#TODO: redbear-udisks — minimal org.freedesktop.UDisks2 daemon. Enumerates block devices from scheme: filesystem.
+[source]
+path = "source"
+
+[build]
+template = "cargo"
+dependencies = ["dbus"]
+
+[package.files]
+"/usr/bin/redbear-udisks" = "redbear-udisks"
diff --git a/local/recipes/system/redbear-udisks/source/Cargo.toml b/local/recipes/system/redbear-udisks/source/Cargo.toml
new file mode 100644
index 00000000..49c8e105
--- /dev/null
+++ b/local/recipes/system/redbear-udisks/source/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "redbear-udisks"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "redbear-udisks"
+path = "src/main.rs"
+
+[dependencies]
+zbus = { version = "5", default-features = false, features = ["tokio"] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/local/recipes/system/redbear-udisks/source/src/interfaces.rs b/local/recipes/system/redbear-udisks/source/src/interfaces.rs
new file mode 100644
index 00000000..dd575801
--- /dev/null
+++ b/local/recipes/system/redbear-udisks/source/src/interfaces.rs
@@ -0,0 +1,218 @@
+use std::{collections::{BTreeMap, HashMap}, sync::Arc};
+
+use zbus::{
+ interface,
+ object_server::SignalEmitter,
+ zvariant::{OwnedObjectPath, Value},
+};
+
+use crate::inventory::{BlockDevice, DriveDevice, Inventory};
+
+type PropertyMap = BTreeMap>;
+type InterfaceMap = BTreeMap;
+pub type ManagedObjects = HashMap;
+
+#[derive(Clone, Debug)]
+pub struct ObjectManagerRoot {
+ inventory: Arc,
+}
+
+#[derive(Clone, Debug)]
+pub struct UDisksManager {
+ inventory: Arc,
+}
+
+#[derive(Clone, Debug)]
+pub struct BlockDeviceInterface {
+ block: BlockDevice,
+}
+
+#[derive(Clone, Debug)]
+pub struct DriveInterface {
+ drive: DriveDevice,
+}
+
+impl ObjectManagerRoot {
+ pub fn new(inventory: Arc) -> Self {
+ Self { inventory }
+ }
+}
+
+impl UDisksManager {
+ pub fn new(inventory: Arc) -> Self {
+ Self { inventory }
+ }
+}
+
+impl BlockDeviceInterface {
+ pub fn new(block: BlockDevice) -> Self {
+ Self { block }
+ }
+}
+
+impl DriveInterface {
+ pub fn new(drive: DriveDevice) -> Self {
+ Self { drive }
+ }
+}
+
+#[interface(name = "org.freedesktop.DBus.ObjectManager")]
+impl ObjectManagerRoot {
+ fn get_managed_objects(&self) -> ManagedObjects {
+ let mut objects = HashMap::new();
+
+ objects.insert(self.inventory.manager_path(), manager_interfaces());
+
+ for drive in self.inventory.drives() {
+ objects.insert(drive.object_path.clone(), drive_interfaces(drive));
+ }
+
+ for block in self.inventory.blocks() {
+ objects.insert(block.object_path.clone(), block_interfaces(block));
+ }
+
+ objects
+ }
+
+ #[zbus(signal, name = "InterfacesAdded")]
+ async fn interfaces_added(
+ signal_emitter: &SignalEmitter<'_>,
+ object_path: OwnedObjectPath,
+ interfaces_and_properties: InterfaceMap,
+ ) -> zbus::Result<()>;
+
+ #[zbus(signal, name = "InterfacesRemoved")]
+ async fn interfaces_removed(
+ signal_emitter: &SignalEmitter<'_>,
+ object_path: OwnedObjectPath,
+ interfaces: Vec,
+ ) -> zbus::Result<()>;
+}
+
+#[interface(name = "org.freedesktop.UDisks2.Manager")]
+impl UDisksManager {
+ fn get_block_devices(&self, _options: HashMap>) -> Vec {
+ self.inventory.block_paths()
+ }
+
+ fn get_drives(&self, _options: HashMap>) -> Vec {
+ self.inventory.drive_paths()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Version")]
+ fn version(&self) -> String {
+ env!("CARGO_PKG_VERSION").to_string()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "SupportedFilesystems")]
+ fn supported_filesystems(&self) -> Vec {
+ Vec::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "SupportedEncryptionTypes")]
+ fn supported_encryption_types(&self) -> Vec {
+ Vec::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "DefaultEncryptionType")]
+ fn default_encryption_type(&self) -> String {
+ String::new()
+ }
+}
+
+#[interface(name = "org.freedesktop.UDisks2.Block")]
+impl BlockDeviceInterface {
+ #[zbus(property(emits_changed_signal = "const"), name = "Device")]
+ fn device(&self) -> Vec {
+ self.block.device_path.as_bytes().to_vec()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "PreferredDevice")]
+ fn preferred_device(&self) -> Vec {
+ self.block.device_path.as_bytes().to_vec()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Symlinks")]
+ fn symlinks(&self) -> Vec> {
+ Vec::new()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Size")]
+ fn size(&self) -> u64 {
+ self.block.size
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "ReadOnly")]
+ fn read_only(&self) -> bool {
+ self.block.read_only
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Drive")]
+ fn drive(&self) -> OwnedObjectPath {
+ self.block.drive_object_path.clone()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "HintPartitionable")]
+ fn hint_partitionable(&self) -> bool {
+ self.block.hint_partitionable
+ }
+}
+
+#[interface(name = "org.freedesktop.UDisks2.Drive")]
+impl DriveInterface {
+ #[zbus(property(emits_changed_signal = "const"), name = "ConnectionBus")]
+ fn connection_bus(&self) -> String {
+ self.drive.scheme_identity.clone()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Size")]
+ fn size(&self) -> u64 {
+ self.drive.size
+ }
+}
+
+fn manager_interfaces() -> InterfaceMap {
+ let mut properties = BTreeMap::new();
+ properties.insert(String::from("Version"), Value::new(env!("CARGO_PKG_VERSION").to_string()));
+ properties.insert(String::from("SupportedFilesystems"), Value::new(Vec::::new()));
+ properties.insert(
+ String::from("SupportedEncryptionTypes"),
+ Value::new(Vec::::new()),
+ );
+ properties.insert(String::from("DefaultEncryptionType"), Value::new(String::new()));
+
+ BTreeMap::from([(String::from("org.freedesktop.UDisks2.Manager"), properties)])
+}
+
+fn drive_interfaces(drive: &DriveDevice) -> InterfaceMap {
+ let mut properties = BTreeMap::new();
+ properties.insert(
+ String::from("ConnectionBus"),
+ Value::new(drive.scheme_identity.clone()),
+ );
+ properties.insert(String::from("Size"), Value::new(drive.size));
+
+ BTreeMap::from([(String::from("org.freedesktop.UDisks2.Drive"), properties)])
+}
+
+fn block_interfaces(block: &BlockDevice) -> InterfaceMap {
+ let mut properties = BTreeMap::new();
+ properties.insert(String::from("Device"), Value::new(block.device_path.as_bytes().to_vec()));
+ properties.insert(
+ String::from("PreferredDevice"),
+ Value::new(block.device_path.as_bytes().to_vec()),
+ );
+ properties.insert(String::from("Symlinks"), Value::new(Vec::>::new()));
+ properties.insert(String::from("Size"), Value::new(block.size));
+ properties.insert(String::from("ReadOnly"), Value::new(block.read_only));
+ properties.insert(
+ String::from("Drive"),
+ Value::new(block.drive_object_path.clone()),
+ );
+ properties.insert(
+ String::from("HintPartitionable"),
+ Value::new(block.hint_partitionable),
+ );
+
+ BTreeMap::from([(String::from("org.freedesktop.UDisks2.Block"), properties)])
+}
diff --git a/local/recipes/system/redbear-udisks/source/src/inventory.rs b/local/recipes/system/redbear-udisks/source/src/inventory.rs
new file mode 100644
index 00000000..f3fd8519
--- /dev/null
+++ b/local/recipes/system/redbear-udisks/source/src/inventory.rs
@@ -0,0 +1,285 @@
+use std::{
+ collections::BTreeMap,
+ fs,
+ path::{Path, PathBuf},
+};
+
+#[cfg(unix)]
+use std::os::unix::fs::MetadataExt;
+
+use zbus::zvariant::OwnedObjectPath;
+
+pub const ROOT_PATH: &str = "/org/freedesktop/UDisks2";
+pub const MANAGER_PATH: &str = "/org/freedesktop/UDisks2/Manager";
+pub const BLOCK_DEVICES_PREFIX: &str = "/org/freedesktop/UDisks2/block_devices";
+pub const DRIVES_PREFIX: &str = "/org/freedesktop/UDisks2/drives";
+
+#[derive(Clone, Debug)]
+pub struct Inventory {
+ manager_path: OwnedObjectPath,
+ drives: Vec,
+ blocks: Vec,
+}
+
+#[derive(Clone, Debug)]
+pub struct DriveDevice {
+ pub object_path: OwnedObjectPath,
+ pub scheme_identity: String,
+ pub size: u64,
+}
+
+#[derive(Clone, Debug)]
+pub struct BlockDevice {
+ pub object_path: OwnedObjectPath,
+ pub drive_object_path: OwnedObjectPath,
+ pub device_path: String,
+ pub size: u64,
+ // UDisks2's base Drive/Block interfaces do not expose logical block size directly,
+ // but Red Bear still derives and retains it from real file metadata.
+ pub logical_block_size: u64,
+ pub read_only: bool,
+ pub hint_partitionable: bool,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+struct RootKey {
+ disk_number: u32,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+struct PartitionKey {
+ disk_number: u32,
+ partition_number: u32,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum EntryKind {
+ Root(RootKey),
+ Partition(PartitionKey),
+}
+
+#[derive(Clone, Copy, Debug)]
+struct DeviceMetadata {
+ size: u64,
+ logical_block_size: u64,
+ read_only: bool,
+}
+
+impl Inventory {
+ pub fn scan() -> Self {
+ let mut drives = Vec::new();
+ let mut blocks = Vec::new();
+
+ for scheme_name in read_dir_names("/scheme")
+ .unwrap_or_default()
+ .into_iter()
+ .filter(|name| name.starts_with("disk."))
+ {
+ let scheme_path = PathBuf::from("/scheme").join(&scheme_name);
+ let scheme_identity = scheme_name
+ .strip_prefix("disk.")
+ .unwrap_or(&scheme_name)
+ .to_string();
+ let entries = read_dir_names(&scheme_path).unwrap_or_default();
+
+ let mut roots = BTreeMap::new();
+ let mut partitions = Vec::new();
+
+ for entry_name in entries {
+ match parse_entry_name(&entry_name) {
+ Some(EntryKind::Root(root_key)) => {
+ roots.insert(root_key, entry_name);
+ }
+ Some(EntryKind::Partition(partition_key)) => {
+ partitions.push((partition_key, entry_name));
+ }
+ None => {}
+ }
+ }
+
+ let mut drive_paths = BTreeMap::new();
+
+ for (root_key, entry_name) in roots {
+ let device_path = format!("{}/{entry_name}", scheme_path.display());
+ let Some(metadata) = read_device_metadata(Path::new(&device_path)) else {
+ continue;
+ };
+
+ let drive = DriveDevice {
+ object_path: owned_object_path(&format!(
+ "{DRIVES_PREFIX}/{}",
+ stable_object_name(&scheme_name, &entry_name)
+ )),
+ scheme_identity: scheme_identity.clone(),
+ size: metadata.size,
+ };
+
+ drive_paths.insert(root_key, drive.object_path.clone());
+
+ blocks.push(BlockDevice {
+ object_path: owned_object_path(&format!(
+ "{BLOCK_DEVICES_PREFIX}/{}",
+ stable_object_name(&scheme_name, &entry_name)
+ )),
+ drive_object_path: drive.object_path.clone(),
+ device_path,
+ size: metadata.size,
+ logical_block_size: metadata.logical_block_size,
+ read_only: metadata.read_only,
+ hint_partitionable: true,
+ });
+
+ drives.push(drive);
+ }
+
+ partitions.sort_by_key(|(partition_key, _)| *partition_key);
+ for (partition_key, entry_name) in partitions {
+ let Some(drive_object_path) = drive_paths.get(&RootKey {
+ disk_number: partition_key.disk_number,
+ }) else {
+ continue;
+ };
+
+ let device_path = format!("{}/{entry_name}", scheme_path.display());
+ let Some(metadata) = read_device_metadata(Path::new(&device_path)) else {
+ continue;
+ };
+
+ blocks.push(BlockDevice {
+ object_path: owned_object_path(&format!(
+ "{BLOCK_DEVICES_PREFIX}/{}",
+ stable_object_name(&scheme_name, &entry_name)
+ )),
+ drive_object_path: drive_object_path.clone(),
+ device_path,
+ size: metadata.size,
+ logical_block_size: metadata.logical_block_size,
+ read_only: metadata.read_only,
+ hint_partitionable: false,
+ });
+ }
+ }
+
+ Self {
+ manager_path: owned_object_path(MANAGER_PATH),
+ drives,
+ blocks,
+ }
+ }
+
+ pub fn manager_path(&self) -> OwnedObjectPath {
+ self.manager_path.clone()
+ }
+
+ pub fn drives(&self) -> &[DriveDevice] {
+ &self.drives
+ }
+
+ pub fn blocks(&self) -> &[BlockDevice] {
+ &self.blocks
+ }
+
+ pub fn drive_paths(&self) -> Vec {
+ self.drives
+ .iter()
+ .map(|drive| drive.object_path.clone())
+ .collect()
+ }
+
+ pub fn block_paths(&self) -> Vec {
+ self.blocks
+ .iter()
+ .map(|block| block.object_path.clone())
+ .collect()
+ }
+}
+
+fn read_dir_names(path: impl AsRef) -> Option> {
+ let mut names = Vec::new();
+ for entry in fs::read_dir(path).ok()? {
+ let entry = entry.ok()?;
+ let name = entry.file_name();
+ let name = name.to_str()?.to_string();
+ names.push(name);
+ }
+ names.sort();
+ Some(names)
+}
+
+fn parse_entry_name(entry_name: &str) -> Option {
+ if let Some(position) = entry_name.find('p') {
+ let disk_number = entry_name[..position].parse().ok()?;
+ let partition_number = entry_name[position + 1..].parse().ok()?;
+ return Some(EntryKind::Partition(PartitionKey {
+ disk_number,
+ partition_number,
+ }));
+ }
+
+ Some(EntryKind::Root(RootKey {
+ disk_number: entry_name.parse().ok()?,
+ }))
+}
+
+fn read_device_metadata(path: &Path) -> Option {
+ let metadata = fs::metadata(path).ok()?;
+ let logical_block_size = metadata_logical_block_size(&metadata);
+
+ Some(DeviceMetadata {
+ size: metadata.len(),
+ logical_block_size,
+ read_only: metadata.permissions().readonly(),
+ })
+}
+
+#[cfg(unix)]
+fn metadata_logical_block_size(metadata: &fs::Metadata) -> u64 {
+ metadata.blksize()
+}
+
+#[cfg(not(unix))]
+fn metadata_logical_block_size(_metadata: &fs::Metadata) -> u64 {
+ 0
+}
+
+fn stable_object_name(scheme_name: &str, entry_name: &str) -> String {
+ format!(
+ "{}_{}",
+ encode_path_component(scheme_name),
+ encode_path_component(entry_name)
+ )
+}
+
+fn encode_path_component(component: &str) -> String {
+ let mut encoded = String::new();
+
+ for byte in component.bytes() {
+ if byte.is_ascii_alphanumeric() {
+ encoded.push(byte as char);
+ } else {
+ encoded.push('_');
+ encoded.push(hex_char(byte >> 4));
+ encoded.push(hex_char(byte & 0x0f));
+ }
+ }
+
+ if encoded.is_empty() {
+ encoded.push('_');
+ encoded.push('0');
+ encoded.push('0');
+ }
+
+ encoded
+}
+
+fn hex_char(value: u8) -> char {
+ match value {
+ 0..=9 => (b'0' + value) as char,
+ 10..=15 => (b'a' + (value - 10)) as char,
+ _ => unreachable!("hex nibble out of range"),
+ }
+}
+
+fn owned_object_path(path: &str) -> OwnedObjectPath {
+ OwnedObjectPath::try_from(path.to_string()).expect("generated object path must be valid")
+}
diff --git a/local/recipes/system/redbear-udisks/source/src/main.rs b/local/recipes/system/redbear-udisks/source/src/main.rs
new file mode 100644
index 00000000..c6952325
--- /dev/null
+++ b/local/recipes/system/redbear-udisks/source/src/main.rs
@@ -0,0 +1,157 @@
+mod interfaces;
+mod inventory;
+
+use std::{
+ env,
+ error::Error,
+ process,
+ sync::Arc,
+};
+
+use interfaces::{BlockDeviceInterface, DriveInterface, ObjectManagerRoot, UDisksManager};
+use inventory::{Inventory, MANAGER_PATH, ROOT_PATH};
+use tokio::runtime::Builder as RuntimeBuilder;
+use zbus::{
+ Address,
+ connection::Builder as ConnectionBuilder,
+ zvariant::OwnedObjectPath,
+};
+
+const BUS_NAME: &str = "org.freedesktop.UDisks2";
+
+enum Command {
+ Run,
+ Help,
+}
+
+fn usage() -> &'static str {
+ "Usage: redbear-udisks [--help]"
+}
+
+fn parse_args() -> Result {
+ let mut args = env::args().skip(1);
+
+ match args.next() {
+ None => Ok(Command::Run),
+ Some(arg) if arg == "--help" || arg == "-h" => {
+ if args.next().is_some() {
+ return Err(String::from("unexpected extra arguments after --help"));
+ }
+
+ Ok(Command::Help)
+ }
+ Some(arg) => Err(format!("unrecognized argument '{arg}'")),
+ }
+}
+
+fn parse_object_path(path: &str) -> Result> {
+ Ok(OwnedObjectPath::try_from(path.to_owned())?)
+}
+
+fn system_connection_builder() -> Result, Box> {
+ if let Ok(address) = env::var("DBUS_STARTER_ADDRESS") {
+ Ok(ConnectionBuilder::address(Address::try_from(address.as_str())?)?)
+ } else {
+ Ok(ConnectionBuilder::system()?)
+ }
+}
+
+#[cfg(all(unix, not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ use tokio::signal::unix::{SignalKind, signal};
+
+ let mut terminate = signal(SignalKind::terminate())?;
+
+ tokio::select! {
+ _ = terminate.recv() => Ok(()),
+ _ = tokio::signal::ctrl_c() => Ok(()),
+ }
+}
+
+#[cfg(target_os = "redox")]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ std::future::pending::<()>().await;
+ #[allow(unreachable_code)]
+ Ok(())
+}
+
+#[cfg(all(not(unix), not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ tokio::signal::ctrl_c().await?;
+ Ok(())
+}
+
+async fn run_daemon() -> Result<(), Box> {
+ eprintln!("redbear-udisks: startup begin");
+ let _root_path = parse_object_path(ROOT_PATH)?;
+ let _manager_path = parse_object_path(MANAGER_PATH)?;
+ eprintln!("redbear-udisks: object paths parsed");
+ let inventory = Arc::new(Inventory::scan());
+ eprintln!(
+ "redbear-udisks: inventory scanned drives={} blocks={}",
+ inventory.drives().len(),
+ inventory.blocks().len()
+ );
+ let block_sizes_known = inventory
+ .blocks()
+ .iter()
+ .filter(|block| block.logical_block_size > 0)
+ .count();
+
+ eprintln!("redbear-udisks: starter address={:?}", env::var("DBUS_STARTER_ADDRESS").ok());
+ eprintln!("redbear-udisks: building D-Bus connection");
+ let mut builder = system_connection_builder()?
+ .name(BUS_NAME)?
+ .serve_at(ROOT_PATH, ObjectManagerRoot::new(inventory.clone()))?
+ .serve_at(MANAGER_PATH, UDisksManager::new(inventory.clone()))?;
+
+ for drive in inventory.drives() {
+ builder = builder.serve_at(drive.object_path.as_str(), DriveInterface::new(drive.clone()))?;
+ }
+
+ for block in inventory.blocks() {
+ builder = builder.serve_at(block.object_path.as_str(), BlockDeviceInterface::new(block.clone()))?;
+ }
+
+ let connection = builder.build().await?;
+
+ eprintln!(
+ "redbear-udisks: registered {BUS_NAME} on the system bus with {} drives, {} block devices, {} metadata-backed block sizes",
+ inventory.drives().len(),
+ inventory.blocks().len(),
+ block_sizes_known,
+ );
+
+ wait_for_shutdown().await?;
+ drop(connection);
+ eprintln!("redbear-udisks: received shutdown signal, exiting cleanly");
+
+ Ok(())
+}
+
+fn main() {
+ match parse_args() {
+ Ok(Command::Help) => {
+ println!("{}", usage());
+ }
+ Ok(Command::Run) => {
+ let runtime = match RuntimeBuilder::new_multi_thread().enable_all().build() {
+ Ok(runtime) => runtime,
+ Err(err) => {
+ eprintln!("redbear-udisks: failed to create tokio runtime: {err}");
+ process::exit(1);
+ }
+ };
+
+ if let Err(err) = runtime.block_on(run_daemon()) {
+ eprintln!("redbear-udisks: fatal error: {err}");
+ process::exit(1);
+ }
+ }
+ Err(err) => {
+ eprintln!("redbear-udisks: {err}");
+ eprintln!("{}", usage());
+ process::exit(1);
+ }
+ }
+}
diff --git a/local/recipes/system/redbear-upower/recipe.toml b/local/recipes/system/redbear-upower/recipe.toml
new file mode 100644
index 00000000..911d6a10
--- /dev/null
+++ b/local/recipes/system/redbear-upower/recipe.toml
@@ -0,0 +1,10 @@
+#TODO: redbear-upower — minimal org.freedesktop.UPower daemon. Enumerates power state from scheme:acpi.
+[source]
+path = "source"
+
+[build]
+template = "cargo"
+dependencies = ["dbus"]
+
+[package.files]
+"/usr/bin/redbear-upower" = "redbear-upower"
diff --git a/local/recipes/system/redbear-upower/source/Cargo.toml b/local/recipes/system/redbear-upower/source/Cargo.toml
new file mode 100644
index 00000000..445e0768
--- /dev/null
+++ b/local/recipes/system/redbear-upower/source/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "redbear-upower"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "redbear-upower"
+path = "src/main.rs"
+
+[dependencies]
+zbus = { version = "5", default-features = false, features = ["tokio"] }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
diff --git a/local/recipes/system/redbear-upower/source/src/main.rs b/local/recipes/system/redbear-upower/source/src/main.rs
new file mode 100644
index 00000000..1727d1e0
--- /dev/null
+++ b/local/recipes/system/redbear-upower/source/src/main.rs
@@ -0,0 +1,540 @@
+use std::{
+ env,
+ error::Error,
+ fs,
+ path::{Path, PathBuf},
+ process,
+};
+
+use tokio::runtime::Builder as RuntimeBuilder;
+use zbus::{
+ Address,
+ connection::Builder as ConnectionBuilder, interface, object_server::SignalEmitter,
+ zvariant::OwnedObjectPath,
+};
+
+const BUS_NAME: &str = "org.freedesktop.UPower";
+const UPOWER_PATH: &str = "/org/freedesktop/UPower";
+const DISPLAY_DEVICE_PATH: &str = "/org/freedesktop/UPower/devices/DisplayDevice";
+const ACPI_POWER_ROOT: &str = "/scheme/acpi/power";
+
+const DEVICE_KIND_UNKNOWN: u32 = 0;
+const DEVICE_KIND_LINE_POWER: u32 = 1;
+const DEVICE_KIND_BATTERY: u32 = 2;
+
+const DEVICE_STATE_UNKNOWN: u32 = 0;
+const DEVICE_STATE_CHARGING: u32 = 1;
+const DEVICE_STATE_DISCHARGING: u32 = 2;
+const DEVICE_STATE_EMPTY: u32 = 3;
+const DEVICE_STATE_FULLY_CHARGED: u32 = 4;
+
+#[derive(Debug, Clone)]
+struct PowerRuntime {
+ root: PathBuf,
+ adapter_ids: Vec,
+ battery_ids: Vec,
+ object_paths: Vec,
+}
+
+#[derive(Debug, Clone)]
+struct UPowerDaemon {
+ runtime: PowerRuntime,
+}
+
+#[derive(Debug, Clone)]
+struct DisplayDevice {
+ runtime: PowerRuntime,
+}
+
+#[derive(Debug, Clone)]
+struct PowerDevice {
+ runtime: PowerRuntime,
+ descriptor: DeviceDescriptor,
+}
+
+#[derive(Debug, Clone)]
+enum DeviceDescriptor {
+ Adapter(String),
+ Battery(String),
+}
+
+#[derive(Debug, Clone)]
+struct AdapterState {
+ native_path: String,
+ online: bool,
+}
+
+#[derive(Debug, Clone)]
+struct BatteryState {
+ native_path: String,
+ state_bits: u64,
+ percentage: Option,
+}
+
+#[derive(Debug, Clone, Default)]
+struct PowerSnapshot {
+ adapters: Vec,
+ batteries: Vec,
+}
+
+enum Command {
+ Run,
+ Help,
+}
+
+fn usage() -> &'static str {
+ "Usage: redbear-upower [--help]"
+}
+
+fn parse_args() -> Result {
+ let mut args = env::args().skip(1);
+
+ match args.next() {
+ None => Ok(Command::Run),
+ Some(arg) if arg == "--help" || arg == "-h" => {
+ if args.next().is_some() {
+ return Err(String::from("unexpected extra arguments after --help"));
+ }
+
+ Ok(Command::Help)
+ }
+ Some(arg) => Err(format!("unrecognized argument '{arg}'")),
+ }
+}
+
+fn parse_object_path(path: &str) -> Result> {
+ Ok(OwnedObjectPath::try_from(path.to_owned())?)
+}
+
+fn system_connection_builder() -> Result, Box> {
+ if let Ok(address) = env::var("DBUS_STARTER_ADDRESS") {
+ Ok(ConnectionBuilder::address(Address::try_from(address.as_str())?)?)
+ } else {
+ Ok(ConnectionBuilder::system()?)
+ }
+}
+
+fn list_dir_names(path: &Path) -> Vec {
+ let mut names = match fs::read_dir(path) {
+ Ok(entries) => entries
+ .filter_map(|entry| entry.ok())
+ .filter_map(|entry| entry.file_name().into_string().ok())
+ .collect::>(),
+ Err(_) => Vec::new(),
+ };
+ names.sort();
+ names
+}
+
+fn read_trimmed(path: impl AsRef) -> Option {
+ let value = fs::read_to_string(path).ok()?;
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ None
+ } else {
+ Some(trimmed.to_string())
+ }
+}
+
+fn read_u64(path: impl AsRef) -> Option {
+ read_trimmed(path)?.parse().ok()
+}
+
+fn read_f64(path: impl AsRef) -> Option {
+ read_trimmed(path)?.parse().ok()
+}
+
+fn battery_state_to_upower(state_bits: u64, percentage: Option) -> u32 {
+ if state_bits & 0x2 != 0 {
+ return DEVICE_STATE_CHARGING;
+ }
+ if state_bits & 0x1 != 0 {
+ return DEVICE_STATE_DISCHARGING;
+ }
+ if state_bits & 0x4 != 0 {
+ return DEVICE_STATE_EMPTY;
+ }
+ if percentage.is_some_and(|value| value >= 99.0) {
+ return DEVICE_STATE_FULLY_CHARGED;
+ }
+
+ DEVICE_STATE_UNKNOWN
+}
+
+impl PowerRuntime {
+ fn discover() -> Result> {
+ let root = PathBuf::from(ACPI_POWER_ROOT);
+ let adapter_ids = list_dir_names(&root.join("adapters"));
+ let battery_ids = list_dir_names(&root.join("batteries"));
+
+ let mut object_paths = Vec::with_capacity(adapter_ids.len() + battery_ids.len());
+ for adapter_id in &adapter_ids {
+ object_paths.push(parse_object_path(&format!(
+ "/org/freedesktop/UPower/devices/line_power_{adapter_id}"
+ ))?);
+ }
+ for battery_id in &battery_ids {
+ object_paths.push(parse_object_path(&format!(
+ "/org/freedesktop/UPower/devices/battery_{battery_id}"
+ ))?);
+ }
+
+ Ok(Self {
+ root,
+ adapter_ids,
+ battery_ids,
+ object_paths,
+ })
+ }
+
+ fn adapter_dir(&self, id: &str) -> PathBuf {
+ self.root.join("adapters").join(id)
+ }
+
+ fn battery_dir(&self, id: &str) -> PathBuf {
+ self.root.join("batteries").join(id)
+ }
+
+ fn read_adapter(&self, id: &str) -> Option {
+ let dir = self.adapter_dir(id);
+ Some(AdapterState {
+ native_path: read_trimmed(dir.join("path"))?,
+ online: read_u64(dir.join("online")).map(|value| value != 0)?,
+ })
+ }
+
+ fn read_battery(&self, id: &str) -> Option {
+ let dir = self.battery_dir(id);
+ Some(BatteryState {
+ native_path: read_trimmed(dir.join("path"))?,
+ state_bits: read_u64(dir.join("state"))?,
+ percentage: read_f64(dir.join("percentage")),
+ })
+ }
+
+ fn snapshot(&self) -> PowerSnapshot {
+ PowerSnapshot {
+ adapters: self
+ .adapter_ids
+ .iter()
+ .filter_map(|id| self.read_adapter(id))
+ .collect(),
+ batteries: self
+ .battery_ids
+ .iter()
+ .filter_map(|id| self.read_battery(id))
+ .collect(),
+ }
+ }
+}
+
+impl PowerSnapshot {
+ fn on_battery(&self) -> bool {
+ if self.adapters.iter().any(|adapter| adapter.online) {
+ return false;
+ }
+
+ self.batteries
+ .iter()
+ .any(|battery| battery.state_bits & 0x1 != 0)
+ }
+
+ fn display_device_kind(&self) -> u32 {
+ if self.batteries.is_empty() {
+ DEVICE_KIND_UNKNOWN
+ } else {
+ DEVICE_KIND_BATTERY
+ }
+ }
+
+ fn display_device_state(&self) -> u32 {
+ if self.batteries.is_empty() {
+ return DEVICE_STATE_UNKNOWN;
+ }
+ if self
+ .batteries
+ .iter()
+ .any(|battery| battery.state_bits & 0x2 != 0)
+ {
+ return DEVICE_STATE_CHARGING;
+ }
+ if self
+ .batteries
+ .iter()
+ .any(|battery| battery.state_bits & 0x1 != 0)
+ {
+ return DEVICE_STATE_DISCHARGING;
+ }
+ if self
+ .batteries
+ .iter()
+ .any(|battery| battery.state_bits & 0x4 != 0)
+ {
+ return DEVICE_STATE_EMPTY;
+ }
+
+ let percentages = self
+ .batteries
+ .iter()
+ .filter_map(|battery| battery.percentage)
+ .collect::>();
+ if !percentages.is_empty() && percentages.iter().all(|value| *value >= 99.0) {
+ return DEVICE_STATE_FULLY_CHARGED;
+ }
+
+ DEVICE_STATE_UNKNOWN
+ }
+
+ fn display_device_percentage(&self) -> f64 {
+ let percentages = self
+ .batteries
+ .iter()
+ .filter_map(|battery| battery.percentage)
+ .collect::>();
+
+ if percentages.is_empty() {
+ 0.0
+ } else {
+ percentages.iter().sum::() / percentages.len() as f64
+ }
+ }
+
+ fn display_device_present(&self) -> bool {
+ !self.batteries.is_empty()
+ }
+}
+
+#[cfg(all(unix, not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ use tokio::signal::unix::{SignalKind, signal};
+
+ let mut terminate = signal(SignalKind::terminate())?;
+
+ tokio::select! {
+ _ = terminate.recv() => Ok(()),
+ _ = tokio::signal::ctrl_c() => Ok(()),
+ }
+}
+
+#[cfg(target_os = "redox")]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ std::future::pending::<()>().await;
+ #[allow(unreachable_code)]
+ Ok(())
+}
+
+#[cfg(all(not(unix), not(target_os = "redox")))]
+async fn wait_for_shutdown() -> Result<(), Box> {
+ tokio::signal::ctrl_c().await?;
+ Ok(())
+}
+
+#[interface(name = "org.freedesktop.UPower")]
+impl UPowerDaemon {
+ fn enumerate_devices(&self) -> Vec {
+ self.runtime.object_paths.clone()
+ }
+
+ fn get_critical_action(&self) -> String {
+ String::from("PowerOff")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "DaemonVersion")]
+ fn daemon_version(&self) -> String {
+ String::from("0.1.0")
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "OnBattery")]
+ fn on_battery(&self) -> bool {
+ self.runtime.snapshot().on_battery()
+ }
+
+ #[zbus(signal, name = "Changed")]
+ async fn changed(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
+}
+
+#[interface(name = "org.freedesktop.UPower.Device")]
+impl DisplayDevice {
+ #[zbus(property(emits_changed_signal = "const"), name = "Type")]
+ fn kind(&self) -> u32 {
+ self.runtime.snapshot().display_device_kind()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "State")]
+ fn state(&self) -> u32 {
+ self.runtime.snapshot().display_device_state()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Percentage")]
+ fn percentage(&self) -> f64 {
+ self.runtime.snapshot().display_device_percentage()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IsPresent")]
+ fn is_present(&self) -> bool {
+ self.runtime.snapshot().display_device_present()
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Online")]
+ fn online(&self) -> bool {
+ false
+ }
+}
+
+#[interface(name = "org.freedesktop.UPower.Device")]
+impl PowerDevice {
+ #[zbus(property(emits_changed_signal = "const"), name = "Type")]
+ fn kind(&self) -> u32 {
+ match self.descriptor {
+ DeviceDescriptor::Adapter(_) => DEVICE_KIND_LINE_POWER,
+ DeviceDescriptor::Battery(_) => DEVICE_KIND_BATTERY,
+ }
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "State")]
+ fn state(&self) -> u32 {
+ match &self.descriptor {
+ DeviceDescriptor::Adapter(_) => DEVICE_STATE_UNKNOWN,
+ DeviceDescriptor::Battery(id) => self
+ .runtime
+ .read_battery(id)
+ .map(|battery| battery_state_to_upower(battery.state_bits, battery.percentage))
+ .unwrap_or(DEVICE_STATE_UNKNOWN),
+ }
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Percentage")]
+ fn percentage(&self) -> f64 {
+ match &self.descriptor {
+ DeviceDescriptor::Adapter(_) => 0.0,
+ DeviceDescriptor::Battery(id) => self
+ .runtime
+ .read_battery(id)
+ .and_then(|battery| battery.percentage)
+ .unwrap_or(0.0),
+ }
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "IsPresent")]
+ fn is_present(&self) -> bool {
+ match &self.descriptor {
+ DeviceDescriptor::Adapter(id) => self.runtime.read_adapter(id).is_some(),
+ DeviceDescriptor::Battery(id) => self.runtime.read_battery(id).is_some(),
+ }
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "Online")]
+ fn online(&self) -> bool {
+ match &self.descriptor {
+ DeviceDescriptor::Adapter(id) => self
+ .runtime
+ .read_adapter(id)
+ .map(|adapter| adapter.online)
+ .unwrap_or(false),
+ DeviceDescriptor::Battery(_) => false,
+ }
+ }
+
+ #[zbus(property(emits_changed_signal = "const"), name = "NativePath")]
+ fn native_path(&self) -> String {
+ match &self.descriptor {
+ DeviceDescriptor::Adapter(id) => self
+ .runtime
+ .read_adapter(id)
+ .map(|adapter| adapter.native_path)
+ .unwrap_or_default(),
+ DeviceDescriptor::Battery(id) => self
+ .runtime
+ .read_battery(id)
+ .map(|battery| battery.native_path)
+ .unwrap_or_default(),
+ }
+ }
+}
+
+async fn run_daemon() -> Result<(), Box> {
+ eprintln!("redbear-upower: startup begin");
+ let runtime = PowerRuntime::discover()?;
+ eprintln!(
+ "redbear-upower: runtime discovered adapters={} batteries={}",
+ runtime.adapter_ids.len(),
+ runtime.battery_ids.len()
+ );
+ let _display_device_path = parse_object_path(DISPLAY_DEVICE_PATH)?;
+ eprintln!("redbear-upower: object paths parsed");
+
+ eprintln!("redbear-upower: starter address={:?}", env::var("DBUS_STARTER_ADDRESS").ok());
+ eprintln!("redbear-upower: building D-Bus connection");
+ let mut builder = system_connection_builder()?
+ .name(BUS_NAME)?
+ .serve_at(
+ UPOWER_PATH,
+ UPowerDaemon {
+ runtime: runtime.clone(),
+ },
+ )?
+ .serve_at(
+ DISPLAY_DEVICE_PATH,
+ DisplayDevice {
+ runtime: runtime.clone(),
+ },
+ )?;
+
+ for adapter_id in &runtime.adapter_ids {
+ let path = format!("/org/freedesktop/UPower/devices/line_power_{adapter_id}");
+ builder = builder.serve_at(
+ path,
+ PowerDevice {
+ runtime: runtime.clone(),
+ descriptor: DeviceDescriptor::Adapter(adapter_id.clone()),
+ },
+ )?;
+ }
+ for battery_id in &runtime.battery_ids {
+ let path = format!("/org/freedesktop/UPower/devices/battery_{battery_id}");
+ builder = builder.serve_at(
+ path,
+ PowerDevice {
+ runtime: runtime.clone(),
+ descriptor: DeviceDescriptor::Battery(battery_id.clone()),
+ },
+ )?;
+ }
+
+ let connection = builder.build().await?;
+
+ eprintln!("redbear-upower: registered {BUS_NAME} on the system bus");
+
+ wait_for_shutdown().await?;
+ drop(connection);
+ eprintln!("redbear-upower: received shutdown signal, exiting cleanly");
+
+ Ok(())
+}
+
+fn main() {
+ match parse_args() {
+ Ok(Command::Help) => {
+ println!("{}", usage());
+ }
+ Ok(Command::Run) => {
+ let runtime = match RuntimeBuilder::new_multi_thread().enable_all().build() {
+ Ok(runtime) => runtime,
+ Err(err) => {
+ eprintln!("redbear-upower: failed to create tokio runtime: {err}");
+ process::exit(1);
+ }
+ };
+
+ if let Err(err) = runtime.block_on(run_daemon()) {
+ eprintln!("redbear-upower: fatal error: {err}");
+ process::exit(1);
+ }
+ }
+ Err(err) => {
+ eprintln!("redbear-upower: {err}");
+ eprintln!("{}", usage());
+ process::exit(1);
+ }
+ }
+}
diff --git a/local/scripts/test-dbus-qemu.sh b/local/scripts/test-dbus-qemu.sh
new file mode 100755
index 00000000..1cc4c27d
--- /dev/null
+++ b/local/scripts/test-dbus-qemu.sh
@@ -0,0 +1,238 @@
+#!/usr/bin/env bash
+# test-dbus-qemu.sh — Validate D-Bus system bus and redbear-sessiond inside a QEMU guest
+#
+# Usage:
+# ./local/scripts/test-dbus-qemu.sh [--check] [--config CONFIG]
+#
+# Options:
+# --check Run non-interactively, exit 0 on pass, 1 on fail
+# --config CONFIG Build config to test (default: redbear-kde)
+#
+# --check mode boots the image, waits for the login prompt, then sends D-Bus
+# validation commands via the serial console. Output is captured and parsed.
+#
+# Checks performed inside the guest:
+# 1. dbus-daemon is running (system bus socket exists)
+# 2. org.freedesktop.login1 is registered on the system bus
+# 3. redbear-sessiond process is running
+# 4. login1.Manager.ListSessions returns session c1
+# 5. login1.Manager.IdleHint property is false
+# 6. login1.Session.Active property is true
+# 7. login1.Seat.CanGraphical property is true
+#
+# Exit codes:
+# 0 All checks passed
+# 1 One or more checks failed
+# 2 Build or QEMU launch failed
+
+set -euo pipefail
+
+CHECK_MODE=0
+CONFIG_NAME="redbear-kde"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --check)
+ CHECK_MODE=1
+ shift
+ ;;
+ --config)
+ CONFIG_NAME="$2"
+ shift 2
+ ;;
+ *)
+ echo "Usage: $0 [--check] [--config CONFIG]" >&2
+ exit 2
+ ;;
+ esac
+done
+
+REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+IMAGE="${REPO_ROOT}/build/${CONFIG_NAME}/x86_64/harddrive.img"
+
+if [[ ! -f "$IMAGE" ]]; then
+ echo "test-dbus-qemu: image not found at ${IMAGE}" >&2
+ echo " Run: make all CONFIG_NAME=${CONFIG_NAME}" >&2
+ exit 2
+fi
+
+GUEST_SCRIPT='/tmp/dbus-check.sh'
+OUTPUT_FILE='/tmp/dbus-qemu-output.txt'
+
+# Build the guest-side check script as a standalone file.
+# Uses org.freedesktop.DBus.Properties.Get for property access (checks 5-7).
+cat > "$GUEST_SCRIPT" <<'GUEST_EOF'
+#!/bin/sh
+echo "=== D-Bus System Bus Validation ==="
+
+# Check 1: dbus-daemon running
+if [ -S /run/dbus/system_bus_socket ]; then
+ echo "PASS: system bus socket exists at /run/dbus/system_bus_socket"
+else
+ echo "FAIL: system bus socket not found at /run/dbus/system_bus_socket"
+fi
+
+# Check 2: org.freedesktop.login1 registered
+if command -v dbus-send >/dev/null 2>&1; then
+ RESULT=$(dbus-send --system --dest=org.freedesktop.DBus \
+ --type=method_call --print-reply \
+ /org/freedesktop/DBus \
+ org.freedesktop.DBus.ListNames 2>&1)
+ if echo "$RESULT" | grep -q "org.freedesktop.login1"; then
+ echo "PASS: org.freedesktop.login1 is registered on the system bus"
+ else
+ echo "FAIL: org.freedesktop.login1 not found on the system bus"
+ echo " Available names: $(echo "$RESULT" | grep string || echo none)"
+ fi
+else
+ echo "SKIP: dbus-send not available (install dbus package)"
+fi
+
+# Check 3: redbear-sessiond process
+if ps | grep -q '[r]edbear-sessiond'; then
+ echo "PASS: redbear-sessiond process is running"
+else
+ echo "FAIL: redbear-sessiond process not found"
+fi
+
+# Check 4: login1.Manager.ListSessions
+if command -v dbus-send >/dev/null 2>&1; then
+ SESSIONS=$(dbus-send --system --dest=org.freedesktop.login1 \
+ --type=method_call --print-reply \
+ /org/freedesktop/login1 \
+ org.freedesktop.login1.Manager.ListSessions 2>&1)
+ if echo "$SESSIONS" | grep -q "c1"; then
+ echo "PASS: ListSessions returns session c1"
+ else
+ echo "FAIL: ListSessions did not return session c1"
+ fi
+fi
+
+# Check 5: login1.Manager.IdleHint (property, not method)
+if command -v dbus-send >/dev/null 2>&1; then
+ IDLE=$(dbus-send --system --dest=org.freedesktop.login1 \
+ --type=method_call --print-reply \
+ /org/freedesktop/login1 \
+ org.freedesktop.DBus.Properties.Get \
+ string:'org.freedesktop.login1.Manager' \
+ string:'IdleHint' 2>&1)
+ if echo "$IDLE" | grep -q "false"; then
+ echo "PASS: Manager.IdleHint = false"
+ else
+ echo "FAIL: Manager.IdleHint not false (got: $IDLE)"
+ fi
+fi
+
+# Check 6: login1.Session.Active (property, not method)
+if command -v dbus-send >/dev/null 2>&1; then
+ ACTIVE=$(dbus-send --system --dest=org.freedesktop.login1 \
+ --type=method_call --print-reply \
+ /org/freedesktop/login1/session/c1 \
+ org.freedesktop.DBus.Properties.Get \
+ string:'org.freedesktop.login1.Session' \
+ string:'Active' 2>&1)
+ if echo "$ACTIVE" | grep -q "true"; then
+ echo "PASS: Session.Active = true"
+ else
+ echo "FAIL: Session.Active not true (got: $ACTIVE)"
+ fi
+fi
+
+# Check 7: login1.Seat.CanGraphical (property, not method)
+if command -v dbus-send >/dev/null 2>&1; then
+ GRAPH=$(dbus-send --system --dest=org.freedesktop.login1 \
+ --type=method_call --print-reply \
+ /org/freedesktop/login1/seat/seat0 \
+ org.freedesktop.DBus.Properties.Get \
+ string:'org.freedesktop.login1.Seat' \
+ string:'CanGraphical' 2>&1)
+ if echo "$GRAPH" | grep -q "true"; then
+ echo "PASS: Seat.CanGraphical = true"
+ else
+ echo "FAIL: Seat.CanGraphical not true (got: $GRAPH)"
+ fi
+fi
+
+echo "=== D-Bus Validation Complete ==="
+GUEST_EOF
+chmod +x "$GUEST_SCRIPT"
+
+if [[ "$CHECK_MODE" -eq 1 ]]; then
+ if ! command -v expect >/dev/null 2>&1; then
+ echo "test-dbus-qemu: --check mode requires 'expect' (install tcllib/expect)" >&2
+ exit 2
+ fi
+
+ echo "test-dbus-qemu: launching QEMU with D-Bus checks (non-interactive)"
+
+ # Use expect to boot the guest, wait for login, run checks, capture output
+ expect <<'EXPECT_EOF' > "$OUTPUT_FILE" 2>&1
+set timeout 120
+spawn qemu-system-x86_64 \
+ -drive file="$::env(IMAGE)" \
+ -m 2G \
+ -smp 2 \
+ -nographic \
+ -no-reboot
+
+expect {
+ "login:" { }
+ timeout { puts "FAIL: timed out waiting for login prompt"; exit 1 }
+}
+
+sleep 2
+send "root\r"
+
+expect {
+ "#" { }
+ timeout { puts "FAIL: timed out waiting for shell prompt"; exit 1 }
+}
+
+sleep 1
+
+# Read and send the check script line by line
+set fp [open "/tmp/dbus-check.sh" r]
+while {[gets $fp line] >= 0} {
+ send "$line\r"
+ expect {
+ "#" { }
+ timeout { puts "FAIL: timed out during check execution"; exit 1 }
+ }
+}
+close $fp
+
+sleep 2
+send "poweroff\r"
+
+expect {
+ "Power down" { }
+ timeout { }
+}
+
+exit 0
+EXPECT_EOF
+
+ FAILED=$(grep -c "FAIL" "$OUTPUT_FILE" 2>/dev/null || true)
+ PASSED=$(grep -c "PASS" "$OUTPUT_FILE" 2>/dev/null || true)
+
+ grep -E "PASS|FAIL|=== D-Bus" "$OUTPUT_FILE" || true
+ echo ""
+ echo "Results: ${PASSED} passed, ${FAILED} failed"
+
+ rm -f "$GUEST_SCRIPT"
+ if [[ "$FAILED" -gt 0 ]]; then
+ exit 1
+ fi
+ exit 0
+else
+ echo "test-dbus-qemu: launching QEMU (interactive mode)"
+ echo " Guest check script written to /tmp/dbus-check.sh"
+ echo " After login, run: sh /tmp/dbus-check.sh"
+ echo ""
+
+ qemu-system-x86_64 \
+ -drive file="$IMAGE",format=raw \
+ -m 2G \
+ -smp 2 \
+ -serial mon:stdio
+fi
diff --git a/recipes/libs/zbus b/recipes/libs/zbus
new file mode 120000
index 00000000..e998126b
--- /dev/null
+++ b/recipes/libs/zbus
@@ -0,0 +1 @@
+../../local/recipes/libs/zbus
\ No newline at end of file
diff --git a/recipes/system/redbear-dbus-services b/recipes/system/redbear-dbus-services
new file mode 120000
index 00000000..100690fc
--- /dev/null
+++ b/recipes/system/redbear-dbus-services
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-dbus-services
\ No newline at end of file
diff --git a/recipes/system/redbear-notifications b/recipes/system/redbear-notifications
new file mode 120000
index 00000000..5837e443
--- /dev/null
+++ b/recipes/system/redbear-notifications
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-notifications
\ No newline at end of file
diff --git a/recipes/system/redbear-polkit b/recipes/system/redbear-polkit
new file mode 120000
index 00000000..b1e71218
--- /dev/null
+++ b/recipes/system/redbear-polkit
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-polkit
\ No newline at end of file
diff --git a/recipes/system/redbear-sessiond b/recipes/system/redbear-sessiond
new file mode 120000
index 00000000..ccb177c7
--- /dev/null
+++ b/recipes/system/redbear-sessiond
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-sessiond
\ No newline at end of file
diff --git a/recipes/system/redbear-udisks b/recipes/system/redbear-udisks
new file mode 120000
index 00000000..02cdb983
--- /dev/null
+++ b/recipes/system/redbear-udisks
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-udisks
\ No newline at end of file
diff --git a/recipes/system/redbear-upower b/recipes/system/redbear-upower
new file mode 120000
index 00000000..b2d15cf0
--- /dev/null
+++ b/recipes/system/redbear-upower
@@ -0,0 +1 @@
+../../local/recipes/system/redbear-upower
\ No newline at end of file