Add D-Bus session and system services

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-17 13:33:17 +01:00
parent 5beb161cd9
commit 60170933b0
58 changed files with 2869 additions and 0 deletions
+12
View File
@@ -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"
"""
+38
View File
@@ -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 = []
@@ -0,0 +1 @@
pub struct Connection;
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.Notifications
Exec=/usr/bin/redbear-notifications
@@ -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
@@ -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
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own="org.kde.*"/>
<allow send_destination="org.kde.*"/>
<allow receive_sender="org.kde.*"/>
<allow own="org.freedesktop.Notifications"/>
<allow send_destination="org.freedesktop.Notifications"/>
<allow receive_sender="org.freedesktop.Notifications"/>
<allow own="org.freedesktop.StatusNotifierWatcher"/>
<allow send_destination="org.freedesktop.StatusNotifierWatcher"/>
<allow receive_sender="org.freedesktop.StatusNotifierWatcher"/>
</policy>
</busconfig>
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.PolicyKit1
Exec=/usr/bin/redbear-polkit
User=root
SystemdService=redbear-polkit.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.UDisks2
Exec=/usr/bin/redbear-udisks
User=root
SystemdService=redbear-udisks.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.UPower
Exec=/usr/bin/redbear-upower
User=root
SystemdService=redbear-upower.service
@@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.freedesktop.login1
Exec=/usr/bin/redbear-sessiond
User=root
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.PolicyKit1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.PolicyKit1"/>
<allow receive_sender="org.freedesktop.PolicyKit1"/>
</policy>
</busconfig>
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.UDisks2"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.UDisks2"/>
<allow receive_sender="org.freedesktop.UDisks2"/>
</policy>
</busconfig>
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.UPower"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.UPower"/>
<allow receive_sender="org.freedesktop.UPower"/>
</policy>
</busconfig>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.login1"/>
<allow send_destination="org.freedesktop.login1"/>
<allow receive_sender="org.freedesktop.login1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Manager"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Session"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Seat"/>
<allow receive_sender="org.freedesktop.login1"/>
</policy>
</busconfig>
@@ -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
"""
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.Notifications
Exec=/usr/bin/redbear-notifications
@@ -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
@@ -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
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own="org.kde.*"/>
<allow send_destination="org.kde.*"/>
<allow receive_sender="org.kde.*"/>
<allow own="org.freedesktop.Notifications"/>
<allow send_destination="org.freedesktop.Notifications"/>
<allow receive_sender="org.freedesktop.Notifications"/>
<allow own="org.freedesktop.StatusNotifierWatcher"/>
<allow send_destination="org.freedesktop.StatusNotifierWatcher"/>
<allow receive_sender="org.freedesktop.StatusNotifierWatcher"/>
</policy>
</busconfig>
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.PolicyKit1
Exec=/usr/bin/redbear-polkit
User=root
SystemdService=redbear-polkit.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.UDisks2
Exec=/usr/bin/redbear-udisks
User=root
SystemdService=redbear-udisks.service
@@ -0,0 +1,5 @@
[D-BUS Service]
Name=org.freedesktop.UPower
Exec=/usr/bin/redbear-upower
User=root
SystemdService=redbear-upower.service
@@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.freedesktop.login1
Exec=/usr/bin/redbear-sessiond
User=root
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.PolicyKit1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.PolicyKit1"/>
<allow receive_sender="org.freedesktop.PolicyKit1"/>
</policy>
</busconfig>
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.UDisks2"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.UDisks2"/>
<allow receive_sender="org.freedesktop.UDisks2"/>
</policy>
</busconfig>
@@ -0,0 +1,11 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.UPower"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.UPower"/>
<allow receive_sender="org.freedesktop.UPower"/>
</policy>
</busconfig>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.login1"/>
<allow send_destination="org.freedesktop.login1"/>
<allow receive_sender="org.freedesktop.login1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Manager"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Session"/>
<allow send_destination="org.freedesktop.login1"
send_interface="org.freedesktop.login1.Seat"/>
<allow receive_sender="org.freedesktop.login1"/>
</policy>
</busconfig>
@@ -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"
@@ -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"] }
@@ -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<String>,
_hints: HashMap<String, Value<'_>>,
_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<String> {
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<Command, String> {
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<dyn Error>> {
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<dyn Error>> {
tokio::signal::ctrl_c().await?;
Ok(())
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
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);
}
}
}
@@ -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"
@@ -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"] }
@@ -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<String, String>;
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<Command, String> {
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<OwnedObjectPath, Box<dyn Error>> {
Ok(OwnedObjectPath::try_from(path.to_owned())?)
}
fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
#[cfg(all(not(unix), not(target_os = "redox")))]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
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<EnumeratedAction> {
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<dyn Error>> {
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);
}
}
}
@@ -0,0 +1,9 @@
[source]
path = "source"
[build]
template = "cargo"
dependencies = ["dbus"]
[package.files]
"/usr/bin/redbear-sessiond" = "redbear-sessiond"
@@ -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" }
@@ -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;
}
}
}
@@ -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<String> {
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<File> {
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)
}
}
@@ -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<Command, String> {
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<OwnedObjectPath, Box<dyn Error>> {
Ok(OwnedObjectPath::try_from(path.to_owned())?)
}
fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
#[cfg(all(not(unix), not(target_os = "redox")))]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
tokio::signal::ctrl_c().await?;
Ok(())
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
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);
}
}
}
@@ -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<OwnedObjectPath> {
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<Vec<(String, u32, String, String, OwnedObjectPath)>> {
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<OwnedObjectPath> {
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<()>;
}
@@ -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<u32>,
}
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<std::sync::MutexGuard<'_, u32>> {
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
}
}
@@ -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<bool>,
taken_devices: Mutex<HashSet<(u32, u32)>>,
}
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<std::sync::MutexGuard<'_, bool>> {
self.controlled
.lock()
.map_err(|_| fdo::Error::Failed(String::from("login1 control state is poisoned")))
}
fn taken_devices(&self) -> fdo::Result<std::sync::MutexGuard<'_, HashSet<(u32, u32)>>> {
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<OwnedFd> {
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<()>;
}
@@ -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"
@@ -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"] }
@@ -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<String, Value<'static>>;
type InterfaceMap = BTreeMap<String, PropertyMap>;
pub type ManagedObjects = HashMap<OwnedObjectPath, InterfaceMap>;
#[derive(Clone, Debug)]
pub struct ObjectManagerRoot {
inventory: Arc<Inventory>,
}
#[derive(Clone, Debug)]
pub struct UDisksManager {
inventory: Arc<Inventory>,
}
#[derive(Clone, Debug)]
pub struct BlockDeviceInterface {
block: BlockDevice,
}
#[derive(Clone, Debug)]
pub struct DriveInterface {
drive: DriveDevice,
}
impl ObjectManagerRoot {
pub fn new(inventory: Arc<Inventory>) -> Self {
Self { inventory }
}
}
impl UDisksManager {
pub fn new(inventory: Arc<Inventory>) -> 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<String>,
) -> zbus::Result<()>;
}
#[interface(name = "org.freedesktop.UDisks2.Manager")]
impl UDisksManager {
fn get_block_devices(&self, _options: HashMap<String, Value<'_>>) -> Vec<OwnedObjectPath> {
self.inventory.block_paths()
}
fn get_drives(&self, _options: HashMap<String, Value<'_>>) -> Vec<OwnedObjectPath> {
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<String> {
Vec::new()
}
#[zbus(property(emits_changed_signal = "const"), name = "SupportedEncryptionTypes")]
fn supported_encryption_types(&self) -> Vec<String> {
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<u8> {
self.block.device_path.as_bytes().to_vec()
}
#[zbus(property(emits_changed_signal = "const"), name = "PreferredDevice")]
fn preferred_device(&self) -> Vec<u8> {
self.block.device_path.as_bytes().to_vec()
}
#[zbus(property(emits_changed_signal = "const"), name = "Symlinks")]
fn symlinks(&self) -> Vec<Vec<u8>> {
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::<String>::new()));
properties.insert(
String::from("SupportedEncryptionTypes"),
Value::new(Vec::<String>::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::<Vec<u8>>::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)])
}
@@ -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<DriveDevice>,
blocks: Vec<BlockDevice>,
}
#[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<OwnedObjectPath> {
self.drives
.iter()
.map(|drive| drive.object_path.clone())
.collect()
}
pub fn block_paths(&self) -> Vec<OwnedObjectPath> {
self.blocks
.iter()
.map(|block| block.object_path.clone())
.collect()
}
}
fn read_dir_names(path: impl AsRef<Path>) -> Option<Vec<String>> {
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<EntryKind> {
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<DeviceMetadata> {
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")
}
@@ -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<Command, String> {
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<OwnedObjectPath, Box<dyn Error>> {
Ok(OwnedObjectPath::try_from(path.to_owned())?)
}
fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
#[cfg(all(not(unix), not(target_os = "redox")))]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
tokio::signal::ctrl_c().await?;
Ok(())
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
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);
}
}
}
@@ -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"
@@ -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"] }
@@ -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<String>,
battery_ids: Vec<String>,
object_paths: Vec<OwnedObjectPath>,
}
#[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<f64>,
}
#[derive(Debug, Clone, Default)]
struct PowerSnapshot {
adapters: Vec<AdapterState>,
batteries: Vec<BatteryState>,
}
enum Command {
Run,
Help,
}
fn usage() -> &'static str {
"Usage: redbear-upower [--help]"
}
fn parse_args() -> Result<Command, String> {
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<OwnedObjectPath, Box<dyn Error>> {
Ok(OwnedObjectPath::try_from(path.to_owned())?)
}
fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Error>> {
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<String> {
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::<Vec<_>>(),
Err(_) => Vec::new(),
};
names.sort();
names
}
fn read_trimmed(path: impl AsRef<Path>) -> Option<String> {
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<Path>) -> Option<u64> {
read_trimmed(path)?.parse().ok()
}
fn read_f64(path: impl AsRef<Path>) -> Option<f64> {
read_trimmed(path)?.parse().ok()
}
fn battery_state_to_upower(state_bits: u64, percentage: Option<f64>) -> 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<Self, Box<dyn Error>> {
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<AdapterState> {
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<BatteryState> {
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::<Vec<_>>();
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::<Vec<_>>();
if percentages.is_empty() {
0.0
} else {
percentages.iter().sum::<f64>() / 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<dyn Error>> {
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<dyn Error>> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
#[cfg(all(not(unix), not(target_os = "redox")))]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
tokio::signal::ctrl_c().await?;
Ok(())
}
#[interface(name = "org.freedesktop.UPower")]
impl UPowerDaemon {
fn enumerate_devices(&self) -> Vec<OwnedObjectPath> {
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<dyn Error>> {
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);
}
}
}
+238
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
../../local/recipes/libs/zbus
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-dbus-services
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-notifications
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-polkit
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-sessiond
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-udisks
+1
View File
@@ -0,0 +1 @@
../../local/recipes/system/redbear-upower