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:
@@ -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"
|
||||||
|
"""
|
||||||
@@ -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;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.Notifications
|
||||||
|
Exec=/usr/bin/redbear-notifications
|
||||||
+4
@@ -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
|
||||||
+4
@@ -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>
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.PolicyKit1
|
||||||
|
Exec=/usr/bin/redbear-polkit
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-polkit.service
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.UDisks2
|
||||||
|
Exec=/usr/bin/redbear-udisks
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-udisks.service
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.UPower
|
||||||
|
Exec=/usr/bin/redbear-upower
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-upower.service
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.login1
|
||||||
|
Exec=/usr/bin/redbear-sessiond
|
||||||
|
User=root
|
||||||
+11
@@ -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>
|
||||||
+11
@@ -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
|
||||||
|
"""
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.Notifications
|
||||||
|
Exec=/usr/bin/redbear-notifications
|
||||||
+4
@@ -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
|
||||||
+4
@@ -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>
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.PolicyKit1
|
||||||
|
Exec=/usr/bin/redbear-polkit
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-polkit.service
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.UDisks2
|
||||||
|
Exec=/usr/bin/redbear-udisks
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-udisks.service
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.UPower
|
||||||
|
Exec=/usr/bin/redbear-upower
|
||||||
|
User=root
|
||||||
|
SystemdService=redbear-upower.service
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
[D-BUS Service]
|
||||||
|
Name=org.freedesktop.login1
|
||||||
|
Exec=/usr/bin/redbear-sessiond
|
||||||
|
User=root
|
||||||
+11
@@ -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>
|
||||||
+11
@@ -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>
|
||||||
+11
@@ -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>
|
||||||
+23
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+238
@@ -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
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/libs/zbus
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-dbus-services
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-notifications
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-polkit
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-sessiond
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-udisks
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/system/redbear-upower
|
||||||
Reference in New Issue
Block a user