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
@@ -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);
}
}
}