D-Bus Phase 3/4: upgrade sessiond, services, add StatusNotifierWatcher, consolidate configs

- redbear-sessiond: add Manager.Inhibit (pipe FD), CanPowerOff/CanReboot/
  CanSuspend/CanHibernate/CanHybridSleep/CanSleep (return na), PowerOff/
  Reboot/Suspend stubs, GetSessionByPID, ListUsers, ListSeats,
  ListInhibitors, ActivateSession/LockSession/UnlockSession/TerminateSession
- redbear-sessiond: add Session SetIdleHint, SetLockedHint, SetType,
  Terminate methods; wire PauseDevice/ResumeDevice/Lock/Unlock signal
  emission via SignalEmitter injection; add dynamic device enumeration
  scanning /scheme/drm/card* and /dev/input/event* at startup
- redbear-sessiond: replace infinite pending() with stoppable shutdown
  via tokio watch channel + control socket shutdown command
- redbear-upower: add Changed signal emission with 30s periodic polling
  and power state snapshot comparison
- redbear-notifications: add ActionInvoked signal, expand capabilities
  to body + body-markup + actions
- redbear-polkit, redbear-udisks: replace pending() with stoppable
  shutdown via signal handling + watch channel
- Add redbear-statusnotifierwatcher: new session bus service implementing
  org.freedesktop.StatusNotifierWatcher for KDE system tray
- Add D-Bus activation file for StatusNotifierWatcher
- KWin session.cpp: try LogindSession before NoopSession fallback
- Consolidate config profiles: remove obsolete redbear-desktop, redbear-kde,
  redbear-live-*, redbear-minimal-*, redbear-wayland configs; simplify
  to three supported targets (redbear-full, redbear-mini, redbear-grub)
- Update DBUS-INTEGRATION-PLAN.md and DESKTOP-STACK-CURRENT-STATUS.md
  with Phase 3/4 fragility assessment, KWin readiness matrix, and
  completeness gap analysis
This commit is contained in:
2026-04-25 12:01:25 +01:00
parent b6d3e1eb9f
commit 20162fccf8
55 changed files with 1535 additions and 1932 deletions
@@ -24,9 +24,6 @@ static const struct
std::unique_ptr<Session> Session::create()
{
#ifdef Q_OS_REDOX
return NoopSession::create();
#else
for (const auto &sessionInfo : s_availableSessions) {
std::unique_ptr<Session> session = sessionInfo.createFunc();
if (session) {
@@ -34,20 +31,19 @@ std::unique_ptr<Session> Session::create()
}
}
return nullptr;
#endif
}
std::unique_ptr<Session> Session::create(Type type)
{
#ifdef Q_OS_REDOX
switch (type) {
case Type::Logind:
return NoopSession::create();
case Type::ConsoleKit:
return ConsoleKitSession::create();
case Type::Noop:
return NoopSession::create();
for (const auto &sessionInfo : s_availableSessions) {
if (sessionInfo.type == type) {
if (auto session = sessionInfo.createFunc()) {
return session;
}
}
}
return NoopSession::create();
#else
for (const auto &sessionInfo : s_availableSessions) {
if (sessionInfo.type == type) {
@@ -0,0 +1,3 @@
[D-BUS Service]
Name=org.freedesktop.StatusNotifierWatcher
Exec=/usr/bin/redbear-statusnotifierwatcher
@@ -40,7 +40,7 @@ impl Notifications {
_app_icon: &str,
summary: &str,
body: &str,
_actions: Vec<String>,
actions: Vec<String>,
_hints: HashMap<String, Value<'_>>,
_expire_timeout: i32,
) -> u32 {
@@ -48,6 +48,10 @@ impl Notifications {
eprintln!("notification: [{app_name}] {summary}: {body}");
for chunk in actions.chunks_exact(2) {
eprintln!("notification {id}: action key '{}'", chunk[0]);
}
id
}
@@ -64,7 +68,11 @@ impl Notifications {
#[zbus(name = "GetCapabilities")]
fn get_capabilities(&self) -> Vec<String> {
vec!["body".to_owned()]
vec![
"body".to_owned(),
"body-markup".to_owned(),
"actions".to_owned(),
]
}
#[zbus(name = "GetServerInformation")]
@@ -88,6 +96,13 @@ impl Notifications {
id: u32,
reason: u32,
) -> zbus::Result<()>;
#[zbus(signal, name = "ActionInvoked")]
async fn action_invoked(
signal_emitter: &SignalEmitter<'_>,
id: u32,
action_key: &str,
) -> zbus::Result<()>;
}
enum Command {
@@ -115,25 +130,32 @@ fn parse_args() -> Result<Command, String> {
}
}
#[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(())
fn spawn_signal_handler(shutdown_tx: tokio::sync::watch::Sender<bool>) {
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = shutdown_tx.send(true);
});
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
spawn_signal_handler(shutdown_tx);
let _connection = ConnectionBuilder::session()?
.name(BUS_NAME)?
.serve_at(OBJECT_PATH, Notifications::new())?
@@ -142,8 +164,8 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
eprintln!("redbear-notifications: registered {BUS_NAME} on the session bus");
wait_for_shutdown().await?;
eprintln!("redbear-notifications: received shutdown signal, exiting cleanly");
let _ = shutdown_rx.changed().await;
eprintln!("redbear-notifications: shutdown signal received, exiting cleanly");
Ok(())
}
@@ -1,4 +1,4 @@
use std::{collections::HashMap, env, error::Error, path::Path, process, thread, time::Duration};
use std::{collections::HashMap, env, error::Error, process, time::Duration};
use tokio::runtime::Builder as RuntimeBuilder;
use zbus::{
@@ -77,29 +77,26 @@ fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Err
}
}
#[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(())
fn spawn_signal_handler(shutdown_tx: tokio::sync::watch::Sender<bool>) {
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = shutdown_tx.send(true);
});
}
#[interface(name = "org.freedesktop.PolicyKit1.Authority")]
@@ -151,6 +148,9 @@ impl PolicyKitAuthority {
async fn run_daemon() -> Result<(), Box<dyn Error>> {
wait_for_dbus_socket().await;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
spawn_signal_handler(shutdown_tx);
let mut last_err = None;
for attempt in 1..=5 {
let _authority_path = parse_object_path(AUTHORITY_PATH)?;
@@ -163,13 +163,16 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
{
Ok(connection) => {
eprintln!("redbear-polkit: registered {BUS_NAME} on the system bus");
wait_for_shutdown().await?;
let _ = shutdown_rx.changed().await;
eprintln!("redbear-polkit: shutdown signal received, exiting cleanly");
drop(connection);
return Ok(());
}
Err(err) => {
if attempt < 5 {
eprintln!("redbear-polkit: attempt {attempt}/5 failed ({err}), retrying in 2s...");
eprintln!(
"redbear-polkit: attempt {attempt}/5 failed ({err}), retrying in 2s..."
);
tokio::time::sleep(Duration::from_secs(2)).await;
}
last_err = Some(err.into());
@@ -3,6 +3,7 @@ use std::{
io::{BufRead, BufReader},
os::unix::{fs::PermissionsExt, net::UnixListener},
path::Path,
sync::Arc,
};
use serde::Deserialize;
@@ -24,14 +25,14 @@ enum ControlMessage {
ResetSession {
vt: u32,
},
Shutdown,
}
fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
let Ok(mut runtime) = runtime.write() else {
eprintln!("redbear-sessiond: runtime state is poisoned");
return;
};
fn apply_message(
runtime: &SharedRuntime,
shutdown_tx: &tokio::sync::watch::Sender<bool>,
message: ControlMessage,
) {
match message {
ControlMessage::SetSession {
username,
@@ -40,6 +41,10 @@ fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
leader,
state,
} => {
let Ok(mut runtime) = runtime.write() else {
eprintln!("redbear-sessiond: runtime state is poisoned");
return;
};
runtime.username = username;
runtime.uid = uid;
runtime.vt = vt;
@@ -48,6 +53,10 @@ fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
runtime.active = true;
}
ControlMessage::ResetSession { vt } => {
let Ok(mut runtime) = runtime.write() else {
eprintln!("redbear-sessiond: runtime state is poisoned");
return;
};
runtime.username = String::from("root");
runtime.uid = 0;
runtime.vt = vt;
@@ -55,10 +64,18 @@ fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
runtime.state = String::from("closing");
runtime.active = true;
}
ControlMessage::Shutdown => {
eprintln!("redbear-sessiond: shutdown requested via control socket");
let _ = shutdown_tx.send(true);
}
}
}
pub fn start_control_socket(runtime: SharedRuntime) {
pub fn start_control_socket(
runtime: SharedRuntime,
shutdown_tx: tokio::sync::watch::Sender<bool>,
) {
let shutdown_tx = Arc::new(shutdown_tx);
std::thread::spawn(move || {
if Path::new(CONTROL_SOCKET_PATH).exists() {
if let Err(err) = fs::remove_file(CONTROL_SOCKET_PATH) {
@@ -79,6 +96,7 @@ pub fn start_control_socket(runtime: SharedRuntime) {
eprintln!("redbear-sessiond: failed to chmod control socket: {err}");
}
let shutdown_ref = Arc::clone(&shutdown_tx);
for stream in listener.incoming() {
let Ok(stream) = stream else {
continue;
@@ -89,7 +107,7 @@ pub fn start_control_socket(runtime: SharedRuntime) {
continue;
}
match serde_json::from_str::<ControlMessage>(line.trim()) {
Ok(message) => apply_message(&runtime, message),
Ok(message) => apply_message(&runtime, &shutdown_ref, message),
Err(err) => eprintln!("redbear-sessiond: invalid control message: {err}"),
}
}
@@ -101,12 +119,18 @@ mod tests {
use super::*;
use crate::runtime_state::shared_runtime;
fn test_shutdown_channel() -> (tokio::sync::watch::Sender<bool>, tokio::sync::watch::Receiver<bool>) {
tokio::sync::watch::channel(false)
}
#[test]
fn set_session_message_updates_runtime_state() {
let runtime = shared_runtime();
let (tx, _rx) = test_shutdown_channel();
apply_message(
&runtime,
&tx,
ControlMessage::SetSession {
username: String::from("user"),
uid: 1000,
@@ -128,9 +152,11 @@ mod tests {
#[test]
fn reset_session_message_restores_root_scaffold() {
let runtime = shared_runtime();
let (tx, _rx) = test_shutdown_channel();
apply_message(
&runtime,
&tx,
ControlMessage::SetSession {
username: String::from("user"),
uid: 1000,
@@ -139,7 +165,7 @@ mod tests {
state: String::from("active"),
},
);
apply_message(&runtime, ControlMessage::ResetSession { vt: 3 });
apply_message(&runtime, &tx, ControlMessage::ResetSession { vt: 3 });
let runtime = runtime.read().expect("runtime lock should remain healthy");
assert_eq!(runtime.username, "root");
@@ -170,7 +196,26 @@ mod tests {
assert_eq!(leader, 99);
assert_eq!(state, "online");
}
ControlMessage::ResetSession { .. } => panic!("expected set_session message"),
ControlMessage::ResetSession { .. } | ControlMessage::Shutdown => {
panic!("expected set_session message")
}
}
}
#[test]
fn shutdown_message_sends_true_on_channel() {
let runtime = shared_runtime();
let (tx, mut rx) = test_shutdown_channel();
apply_message(&runtime, &tx, ControlMessage::Shutdown);
assert!(*rx.borrow_and_update());
}
#[test]
fn shutdown_message_parses_from_json() {
let message = serde_json::from_str::<ControlMessage>(r#"{"type":"shutdown"}"#)
.expect("shutdown message should parse");
assert!(matches!(message, ControlMessage::Shutdown));
}
}
@@ -14,6 +14,7 @@ pub struct DeviceMap {
}
impl DeviceMap {
#[cfg(test)]
pub fn new() -> Self {
let static_paths = HashMap::from([
((226, 0), String::from("/scheme/drm/card0")),
@@ -31,6 +32,31 @@ impl DeviceMap {
Self { static_paths }
}
/// Build a device map that merges static entries with dynamically discovered
/// devices by scanning `/scheme/drm/card*` and `/dev/input/event*` at startup.
/// For each discovered path, stat is used to read the rdev (device number).
/// Entries with a nonzero rdev are inserted into the map; static entries are
/// kept as fallback when rdev is unavailable or zero.
pub fn discover() -> Self {
let mut 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")),
]);
discover_scheme_drm(&mut paths);
discover_dev_input(&mut paths);
Self { static_paths: 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());
@@ -83,6 +109,74 @@ impl DeviceMap {
}
}
/// Scan `/scheme/drm/` for `card*` entries and merge any with a nonzero rdev
/// into the provided map. Static entries are not overwritten.
fn discover_scheme_drm(paths: &mut HashMap<(u32, u32), String>) {
let entries = match fs::read_dir("/scheme/drm") {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.starts_with("card") {
continue;
}
#[cfg(unix)]
if let Ok(metadata) = fs::metadata(&path) {
let rdev = metadata.rdev();
if rdev != 0 {
let major = dev_major(rdev);
let minor = dev_minor(rdev);
paths
.entry((major, minor))
.or_insert_with(|| path.to_string_lossy().into_owned());
}
}
#[cfg(not(unix))]
let _ = &path;
}
}
/// Scan `/dev/input/` for `event*` entries and merge any with a nonzero rdev
/// into the provided map. Static entries are not overwritten.
fn discover_dev_input(paths: &mut HashMap<(u32, u32), String>) {
let entries = match fs::read_dir("/dev/input") {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.starts_with("event") {
continue;
}
#[cfg(unix)]
if let Ok(metadata) = fs::metadata(&path) {
let rdev = metadata.rdev();
if rdev != 0 {
let major = dev_major(rdev);
let minor = dev_minor(rdev);
paths
.entry((major, minor))
.or_insert_with(|| path.to_string_lossy().into_owned());
}
}
#[cfg(not(unix))]
let _ = &path;
}
}
fn candidate_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
@@ -162,4 +256,12 @@ mod tests {
assert_eq!(dev_major(event), 13);
assert_eq!(dev_minor(event), 67);
}
#[test]
fn discover_returns_static_entries_when_no_dirs() {
let map = super::DeviceMap::discover();
assert!(map.resolve(226, 0).is_some());
assert!(map.resolve(13, 64).is_some());
assert!(map.resolve(29, 0).is_some());
}
}
@@ -84,7 +84,7 @@ fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Err
}
#[cfg(all(unix, not(target_os = "redox")))]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
async fn wait_for_shutdown(mut shutdown_rx: tokio::sync::watch::Receiver<bool>) -> Result<(), Box<dyn Error>> {
use tokio::signal::unix::{SignalKind, signal};
let mut terminate = signal(SignalKind::terminate())?;
@@ -92,35 +92,44 @@ async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
tokio::select! {
_ = terminate.recv() => Ok(()),
_ = tokio::signal::ctrl_c() => Ok(()),
_ = shutdown_rx.changed() => Ok(()),
}
}
#[cfg(target_os = "redox")]
async fn wait_for_shutdown() -> Result<(), Box<dyn Error>> {
std::future::pending::<()>().await;
async fn wait_for_shutdown(mut shutdown_rx: tokio::sync::watch::Receiver<bool>) -> Result<(), Box<dyn Error>> {
tokio::select! {
_ = std::future::pending::<()>() => Ok(()),
_ = shutdown_rx.changed() => Ok(()),
}
#[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 wait_for_shutdown(mut shutdown_rx: tokio::sync::watch::Receiver<bool>) -> Result<(), Box<dyn Error>> {
tokio::select! {
_ = tokio::signal::ctrl_c() => Ok(()),
_ = shutdown_rx.changed() => Ok(()),
}
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
wait_for_dbus_socket().await;
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let mut last_err = None;
for attempt in 1..=5 {
let session_path = parse_object_path(SESSION_PATH)?;
let seat_path = parse_object_path(SEAT_PATH)?;
let user_path = parse_object_path(USER_PATH)?;
let runtime = shared_runtime();
let device_map = DeviceMap::discover();
let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new(), runtime.clone());
let session = LoginSession::new(seat_path.clone(), user_path.clone(), device_map, runtime.clone());
let seat = LoginSeat::new(session_path.clone(), runtime.clone());
let manager = LoginManager::new(session_path, seat_path, runtime.clone());
let manager = LoginManager::new(session_path, seat_path, user_path, runtime.clone());
match system_connection_builder()?
.name(BUS_NAME)?
@@ -132,9 +141,9 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
{
Ok(connection) => {
eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus");
control::start_control_socket(runtime.clone());
control::start_control_socket(runtime.clone(), shutdown_tx.clone());
tokio::spawn(acpi_watcher::watch_and_emit(connection.clone(), runtime.clone()));
wait_for_shutdown().await?;
wait_for_shutdown(shutdown_rx).await?;
drop(connection);
return Ok(());
}
@@ -1,36 +1,55 @@
use std::{
os::fd::OwnedFd as StdOwnedFd,
os::unix::net::UnixStream,
sync::{Arc, Mutex},
};
use zbus::{
fdo,
interface,
object_server::SignalEmitter,
zvariant::OwnedObjectPath,
zvariant::{OwnedFd, OwnedObjectPath},
};
use crate::runtime_state::SharedRuntime;
use crate::runtime_state::{InhibitorEntry, SharedRuntime};
#[derive(Clone, Debug)]
pub struct LoginManager {
runtime: SharedRuntime,
session_path: OwnedObjectPath,
seat_path: OwnedObjectPath,
user_path: OwnedObjectPath,
inhibitor_fds: Arc<Mutex<Vec<StdOwnedFd>>>,
}
impl LoginManager {
pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath, runtime: SharedRuntime) -> Self {
pub fn new(
session_path: OwnedObjectPath,
seat_path: OwnedObjectPath,
user_path: OwnedObjectPath,
runtime: SharedRuntime,
) -> Self {
Self {
runtime,
session_path,
seat_path,
user_path,
inhibitor_fds: Arc::new(Mutex::new(Vec::new())),
}
}
fn runtime_read(&self) -> fdo::Result<std::sync::RwLockReadGuard<'_, crate::runtime_state::SessionRuntime>> {
self.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))
}
}
#[interface(name = "org.freedesktop.login1.Manager")]
impl LoginManager {
fn get_session(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
let runtime = self.runtime_read()?;
if id == runtime.session_id || id == "auto" {
return Ok(self.session_path.clone());
}
@@ -39,10 +58,7 @@ impl LoginManager {
}
fn list_sessions(&self) -> fdo::Result<Vec<(String, u32, String, String, OwnedObjectPath)>> {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
let runtime = self.runtime_read()?;
Ok(vec![(
runtime.session_id.clone(),
runtime.uid,
@@ -53,10 +69,7 @@ impl LoginManager {
}
fn get_seat(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
let runtime = self
.runtime
.read()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
let runtime = self.runtime_read()?;
if id == runtime.seat_id {
return Ok(self.seat_path.clone());
}
@@ -64,9 +77,151 @@ impl LoginManager {
Err(fdo::Error::Failed(format!("unknown login1 seat '{id}'")))
}
fn inhibit(&self, what: &str, who: &str, why: &str, mode: &str) -> fdo::Result<OwnedFd> {
if mode != "block" && mode != "delay" {
return Err(fdo::Error::Failed(format!(
"inhibit mode must be 'block' or 'delay', got '{mode}'"
)));
}
let (end_caller, end_daemon) = UnixStream::pair()
.map_err(|err| fdo::Error::Failed(format!("failed to create inhibit pipe: {err}")))?;
let fd_caller: StdOwnedFd = end_caller.into();
let fd_daemon: StdOwnedFd = end_daemon.into();
let uid = self.runtime_read().map(|r| r.uid).unwrap_or(0);
let pid = std::process::id();
let entry = InhibitorEntry {
what: what.to_owned(),
who: who.to_owned(),
why: why.to_owned(),
mode: mode.to_owned(),
pid,
uid,
};
if let Ok(mut runtime) = self.runtime.write() {
runtime.inhibitors.push(entry);
}
if let Ok(mut fds) = self.inhibitor_fds.lock() {
fds.push(fd_daemon);
}
eprintln!(
"redbear-sessiond: Inhibit(what={what}, who={who}, mode={mode}) granted"
);
Ok(OwnedFd::from(fd_caller))
}
fn can_power_off(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_reboot(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_suspend(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_hibernate(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_hybrid_sleep(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_suspend_then_hibernate(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn can_sleep(&self) -> fdo::Result<String> {
Ok(String::from("na"))
}
fn power_off(&self, _interactive: bool) -> fdo::Result<()> {
eprintln!("redbear-sessiond: PowerOff requested");
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = true;
}
Ok(())
}
fn reboot(&self, _interactive: bool) -> fdo::Result<()> {
eprintln!("redbear-sessiond: Reboot requested");
Ok(())
}
fn suspend(&self, _interactive: bool) -> fdo::Result<()> {
eprintln!("redbear-sessiond: Suspend requested");
Ok(())
}
fn get_session_by_pid(&self, _pid: u32) -> fdo::Result<OwnedObjectPath> {
Ok(self.session_path.clone())
}
fn list_users(&self) -> fdo::Result<Vec<(u32, String, OwnedObjectPath)>> {
let runtime = self.runtime_read()?;
Ok(vec![(
runtime.uid,
runtime.username.clone(),
self.user_path.clone(),
)])
}
fn list_seats(&self) -> fdo::Result<Vec<(String, OwnedObjectPath)>> {
let runtime = self.runtime_read()?;
Ok(vec![(runtime.seat_id.clone(), self.seat_path.clone())])
}
fn list_inhibitors(&self) -> fdo::Result<Vec<(String, String, String, String, u32, u32)>> {
let runtime = self.runtime_read()?;
Ok(runtime
.inhibitors
.iter()
.map(|entry| {
(
entry.what.clone(),
entry.who.clone(),
entry.why.clone(),
entry.mode.clone(),
entry.pid,
entry.uid,
)
})
.collect())
}
fn activate_session(&self, session_id: &str) -> fdo::Result<()> {
eprintln!("redbear-sessiond: ActivateSession({session_id}) — no-op");
Ok(())
}
fn lock_session(&self, session_id: &str) -> fdo::Result<()> {
eprintln!("redbear-sessiond: LockSession({session_id})");
Ok(())
}
fn unlock_session(&self, session_id: &str) -> fdo::Result<()> {
eprintln!("redbear-sessiond: UnlockSession({session_id})");
Ok(())
}
fn terminate_session(&self, session_id: &str) -> fdo::Result<()> {
eprintln!("redbear-sessiond: TerminateSession({session_id})");
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
fn idle_hint(&self) -> bool {
false
self.runtime_read().map(|r| r.idle_hint).unwrap_or(false)
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleSinceHint")]
@@ -81,12 +236,30 @@ impl LoginManager {
#[zbus(property(emits_changed_signal = "const"), name = "BlockInhibited")]
fn block_inhibited(&self) -> String {
String::new()
self.runtime_read()
.map(|r| {
r.inhibitors
.iter()
.filter(|i| i.mode == "block")
.map(|i| i.what.as_str())
.collect::<Vec<&str>>()
.join(":")
})
.unwrap_or_default()
}
#[zbus(property(emits_changed_signal = "const"), name = "DelayInhibited")]
fn delay_inhibited(&self) -> String {
String::new()
self.runtime_read()
.map(|r| {
r.inhibitors
.iter()
.filter(|i| i.mode == "delay")
.map(|i| i.what.as_str())
.collect::<Vec<&str>>()
.join(":")
})
.unwrap_or_default()
}
#[zbus(property(emits_changed_signal = "const"), name = "InhibitDelayMaxUSec")]
@@ -111,8 +284,7 @@ impl LoginManager {
#[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")]
fn preparing_for_shutdown(&self) -> bool {
self.runtime
.read()
self.runtime_read()
.map(|runtime| runtime.preparing_for_shutdown)
.unwrap_or(false)
}
@@ -132,15 +304,21 @@ mod tests {
use super::*;
use crate::runtime_state::shared_runtime;
#[test]
fn get_session_accepts_runtime_session_id() {
let manager = LoginManager::new(
fn test_manager() -> LoginManager {
LoginManager::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1"))
.expect("session path should parse"),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0"))
.expect("seat path should parse"),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current"))
.expect("user path should parse"),
shared_runtime(),
);
)
}
#[test]
fn get_session_accepts_runtime_session_id() {
let manager = test_manager();
let path = manager
.get_session("c1")
@@ -150,13 +328,7 @@ mod tests {
#[test]
fn get_session_accepts_auto_alias() {
let manager = LoginManager::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1"))
.expect("session path should parse"),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0"))
.expect("seat path should parse"),
shared_runtime(),
);
let manager = test_manager();
let path = manager
.get_session("auto")
@@ -177,9 +349,127 @@ mod tests {
.expect("session path should parse"),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0"))
.expect("seat path should parse"),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current"))
.expect("user path should parse"),
runtime,
);
assert!(manager.preparing_for_shutdown());
}
#[test]
fn can_methods_return_na() {
let manager = test_manager();
assert_eq!(manager.can_power_off().unwrap(), "na");
assert_eq!(manager.can_reboot().unwrap(), "na");
assert_eq!(manager.can_suspend().unwrap(), "na");
assert_eq!(manager.can_hibernate().unwrap(), "na");
assert_eq!(manager.can_hybrid_sleep().unwrap(), "na");
assert_eq!(manager.can_suspend_then_hibernate().unwrap(), "na");
assert_eq!(manager.can_sleep().unwrap(), "na");
}
#[test]
fn list_users_returns_runtime_user() {
let runtime = shared_runtime();
runtime.write().expect("lock").username = String::from("testuser");
runtime.write().expect("lock").uid = 1000;
let manager = LoginManager::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
runtime,
);
let users = manager.list_users().expect("list_users should succeed");
assert_eq!(users.len(), 1);
assert_eq!(users[0].0, 1000);
assert_eq!(users[0].1, "testuser");
}
#[test]
fn list_seats_returns_runtime_seat() {
let manager = test_manager();
let seats = manager.list_seats().expect("list_seats should succeed");
assert_eq!(seats.len(), 1);
assert_eq!(seats[0].0, "seat0");
}
#[test]
fn get_session_by_pid_returns_session_path() {
let manager = test_manager();
let path = manager.get_session_by_pid(1234).expect("should succeed");
assert_eq!(path.as_str(), "/org/freedesktop/login1/session/c1");
}
#[test]
fn list_inhibitors_empty_by_default() {
let manager = test_manager();
let inhibitors = manager.list_inhibitors().expect("should succeed");
assert!(inhibitors.is_empty());
}
#[test]
fn inhibit_rejects_invalid_mode() {
let manager = test_manager();
let err = manager.inhibit("sleep", "test", "reason", "invalid").unwrap_err();
match err {
fdo::Error::Failed(msg) => assert!(msg.contains("block") || msg.contains("delay")),
other => panic!("expected Failed error, got {other:?}"),
}
}
#[test]
fn inhibit_tracks_entry_in_runtime() {
let runtime = shared_runtime();
let manager = LoginManager::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
runtime.clone(),
);
let _fd = manager
.inhibit("sleep", "testapp", "testing", "block")
.expect("inhibit should succeed");
let runtime_guard = runtime.read().expect("lock");
assert_eq!(runtime_guard.inhibitors.len(), 1);
assert_eq!(runtime_guard.inhibitors[0].what, "sleep");
assert_eq!(runtime_guard.inhibitors[0].who, "testapp");
assert_eq!(runtime_guard.inhibitors[0].mode, "block");
}
#[test]
fn block_inhibited_joins_what_fields() {
let runtime = shared_runtime();
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
what: String::from("sleep"),
who: String::from("app1"),
why: String::from("r"),
mode: String::from("block"),
pid: 1,
uid: 0,
});
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
what: String::from("shutdown"),
who: String::from("app2"),
why: String::from("r"),
mode: String::from("block"),
pid: 2,
uid: 0,
});
let manager = LoginManager::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/session/c1")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
runtime,
);
let blocked = manager.block_inhibited();
assert!(blocked.contains("sleep"));
assert!(blocked.contains("shutdown"));
}
}
@@ -1,5 +1,15 @@
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct InhibitorEntry {
pub what: String,
pub who: String,
pub why: String,
pub mode: String,
pub pid: u32,
pub uid: u32,
}
#[derive(Clone, Debug)]
pub struct SessionRuntime {
pub session_id: String,
@@ -11,6 +21,10 @@ pub struct SessionRuntime {
pub state: String,
pub active: bool,
pub preparing_for_shutdown: bool,
pub idle_hint: bool,
pub locked_hint: bool,
pub session_type: String,
pub inhibitors: Vec<InhibitorEntry>,
}
impl Default for SessionRuntime {
@@ -25,6 +39,10 @@ impl Default for SessionRuntime {
state: String::from("online"),
active: true,
preparing_for_shutdown: false,
idle_hint: false,
locked_hint: false,
session_type: String::from("wayland"),
inhibitors: Vec::new(),
}
}
}
@@ -151,6 +151,54 @@ impl LoginSession {
Ok(())
}
fn set_idle_hint(&self, idle: bool) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.idle_hint = idle;
}
eprintln!("redbear-sessiond: SetIdleHint({idle}) for session {session_id}");
Ok(())
}
fn set_locked_hint(&self, locked: bool) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.locked_hint = locked;
}
eprintln!("redbear-sessiond: SetLockedHint({locked}) for session {session_id}");
Ok(())
}
fn set_type(&self, session_type: &str) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.session_type = session_type.to_owned();
}
eprintln!("redbear-sessiond: SetType({session_type}) for session {session_id}");
Ok(())
}
fn terminate(&self) -> fdo::Result<()> {
let runtime = self.runtime()?;
let session_id = runtime.session_id.clone();
drop(runtime);
if let Ok(mut guard) = self.runtime.write() {
guard.state = String::from("closing");
}
eprintln!("redbear-sessiond: Terminate requested for session {session_id}");
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "Active")]
fn active(&self) -> bool {
self.runtime().map(|runtime| runtime.active).unwrap_or(true)
@@ -161,9 +209,11 @@ impl LoginSession {
false
}
#[zbus(property(emits_changed_signal = "const"), name = "Type")]
#[zbus(property(emits_changed_signal = "false"), name = "Type")]
fn kind(&self) -> String {
String::from("wayland")
self.runtime()
.map(|r| r.session_type.clone())
.unwrap_or_else(|_| String::from("wayland"))
}
#[zbus(property(emits_changed_signal = "const"), name = "Class")]
@@ -191,7 +241,7 @@ impl LoginSession {
self.runtime().map(|runtime| runtime.session_id).unwrap_or_else(|_| String::from("c1"))
}
#[zbus(property(emits_changed_signal = "const"), name = "State")]
#[zbus(property(emits_changed_signal = "false"), name = "State")]
fn state(&self) -> String {
self.runtime().map(|runtime| runtime.state).unwrap_or_else(|_| String::from("online"))
}
@@ -244,14 +294,14 @@ impl LoginSession {
String::new()
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
#[zbus(property(emits_changed_signal = "false"), name = "IdleHint")]
fn idle_hint(&self) -> bool {
false
self.runtime().map(|r| r.idle_hint).unwrap_or(false)
}
#[zbus(property(emits_changed_signal = "const"), name = "LockedHint")]
#[zbus(property(emits_changed_signal = "false"), name = "LockedHint")]
fn locked_hint(&self) -> bool {
false
self.runtime().map(|r| r.locked_hint).unwrap_or(false)
}
#[zbus(signal, name = "PauseDevice")]
@@ -269,4 +319,108 @@ impl LoginSession {
minor: u32,
fd: Fd<'_>,
) -> zbus::Result<()>;
#[zbus(signal, name = "Lock")]
async fn lock(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
#[zbus(signal, name = "Unlock")]
async fn unlock(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device_map::DeviceMap;
use crate::runtime_state::shared_runtime;
fn test_session() -> LoginSession {
LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
shared_runtime(),
)
}
#[test]
fn set_idle_hint_updates_runtime() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert!(!session.idle_hint());
session.set_idle_hint(true).unwrap();
assert!(session.idle_hint());
let guard = runtime.read().expect("lock");
assert!(guard.idle_hint);
}
#[test]
fn set_locked_hint_updates_runtime() {
let session = test_session();
assert!(!session.locked_hint());
session.set_locked_hint(true).unwrap();
assert!(session.locked_hint());
}
#[test]
fn set_type_updates_runtime() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert_eq!(session.kind(), "wayland");
session.set_type("x11").unwrap();
assert_eq!(session.kind(), "x11");
let guard = runtime.read().expect("lock");
assert_eq!(guard.session_type, "x11");
}
#[test]
fn terminate_sets_state_to_closing() {
let runtime = shared_runtime();
let session = LoginSession::new(
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/seat/seat0")).unwrap(),
OwnedObjectPath::try_from(String::from("/org/freedesktop/login1/user/current")).unwrap(),
DeviceMap::new(),
runtime.clone(),
);
assert_eq!(session.state(), "online");
session.terminate().unwrap();
assert_eq!(session.state(), "closing");
}
#[test]
fn take_control_then_take_device_rejects_duplicate() {
let session = test_session();
session.take_control(false).unwrap();
session.taken_devices().unwrap().insert((226, 0));
let err = session.take_device(226, 0).unwrap_err();
match err {
fdo::Error::Failed(msg) => assert!(msg.contains("already taken")),
other => panic!("expected Failed error, got {other:?}"),
}
}
#[test]
fn release_device_rejects_unknown() {
let session = test_session();
session.take_control(false).unwrap();
let err = session.release_device(226, 99).unwrap_err();
match err {
fdo::Error::Failed(msg) => assert!(msg.contains("was not taken")),
other => panic!("expected Failed error, got {other:?}"),
}
}
}
@@ -0,0 +1,2 @@
[build]
template = "cargo"
@@ -0,0 +1,12 @@
[package]
name = "redbear-statusnotifierwatcher"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "redbear-statusnotifierwatcher"
path = "src/main.rs"
[dependencies]
zbus = { version = "5", default-features = false, features = ["tokio"] }
tokio = { version = "1", features = ["full"] }
@@ -0,0 +1,168 @@
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use zbus::{
fdo::{self, ObjectManager},
interface,
object_server::SignalEmitter,
connection::Builder as ConnectionBuilder,
zvariant::ObjectPath,
};
const BUS_NAME: &str = "org.freedesktop.StatusNotifierWatcher";
const OBJECT_PATH: &str = "/StatusNotifierWatcher";
/// org.freedesktop.StatusNotifierWatcher D-Bus interface
/// Tracks registered system tray items and hosts for KDE Plasma.
struct StatusNotifierWatcher {
items: Arc<Mutex<HashSet<String>>>,
hosts: Arc<Mutex<HashSet<String>>>,
}
#[interface(name = "org.freedesktop.StatusNotifierWatcher")]
impl StatusNotifierWatcher {
// --- Methods ---
/// Register a status notifier item.
/// The item parameter is either a full object path (e.g., "/org/example/Item")
/// sent by the item itself, or a bus name (e.g., ":1.42" or "org.example.App")
/// sent via the KDE protocol extension.
async fn register_status_notifier_item(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
item: &str,
) -> fdo::Result<()> {
let is_new = {
let mut items = self.items.lock().map_err(|e| {
fdo::Error::Failed(format!("items lock poisoned: {e}"))
})?;
items.insert(item.to_owned())
};
if is_new {
eprintln!("statusnotifierwatcher: item registered: {item}");
let _ = Self::status_notifier_item_registered(&signal_emitter, item).await;
}
Ok(())
}
/// Register a status notifier host (typically the system tray panel).
async fn register_status_notifier_host(
&self,
#[zbus(signal_emitter)] signal_emitter: SignalEmitter<'_>,
host: &str,
) -> fdo::Result<()> {
let is_new = {
let mut hosts = self.hosts.lock().map_err(|e| {
fdo::Error::Failed(format!("hosts lock poisoned: {e}"))
})?;
hosts.insert(host.to_owned())
};
if is_new {
eprintln!("statusnotifierwatcher: host registered: {host}");
}
Ok(())
}
// --- Properties ---
/// List of registered status notifier item bus names / paths.
#[zbus(property)]
fn registered_status_notifier_items(&self) -> fdo::Result<Vec<String>> {
let items = self.items.lock().map_err(|e| {
fdo::Error::Failed(format!("items lock poisoned: {e}"))
})?;
Ok(items.iter().cloned().collect())
}
/// Whether at least one status notifier host is registered.
#[zbus(property)]
fn is_status_notifier_host_registered(&self) -> fdo::Result<bool> {
let hosts = self.hosts.lock().map_err(|e| {
fdo::Error::Failed(format!("hosts lock poisoned: {e}"))
})?;
Ok(!hosts.is_empty())
}
/// Protocol version (always 0 per spec).
#[zbus(property)]
fn protocol_version(&self) -> i32 {
0
}
// --- Signals ---
/// Emitted when a new status notifier item is registered.
#[zbus(signal, name = "StatusNotifierItemRegistered")]
async fn status_notifier_item_registered(
signal_emitter: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
/// Emitted when a status notifier item is unregistered.
#[zbus(signal, name = "StatusNotifierItemUnregistered")]
async fn status_notifier_item_unregistered(
signal_emitter: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
}
async fn wait_for_session_bus() {
for _ in 0..30 {
if std::env::var("DBUS_SESSION_BUS_ADDRESS").is_ok() {
return;
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
wait_for_session_bus().await;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
// Signal handler task for clean shutdown
let signal_tx = shutdown_tx.clone();
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = signal_tx.send(true);
});
// Keep original sender alive so receiver doesn't see all-senders-dropped
let _shutdown_guard = shutdown_tx;
let watcher = StatusNotifierWatcher {
items: Arc::new(Mutex::new(HashSet::new())),
hosts: Arc::new(Mutex::new(HashSet::new())),
};
let path: ObjectPath<'_> = OBJECT_PATH.try_into()?;
let connection = ConnectionBuilder::session()?
.name(BUS_NAME)?
.serve_at(path, watcher)?
.build()
.await?;
eprintln!("statusnotifierwatcher: {BUS_NAME} registered on session bus");
// Wait for shutdown signal
let _ = shutdown_rx.changed().await;
eprintln!("statusnotifierwatcher: shutdown signal received, exiting cleanly");
drop(connection);
Ok(())
}
@@ -4,10 +4,8 @@ mod inventory;
use std::{
env,
error::Error,
path::Path,
process,
sync::Arc,
thread,
time::Duration,
};
@@ -74,34 +72,34 @@ fn system_connection_builder() -> Result<ConnectionBuilder<'static>, Box<dyn Err
}
}
#[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(())
fn spawn_signal_handler(shutdown_tx: tokio::sync::watch::Sender<bool>) {
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = shutdown_tx.send(true);
});
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
wait_for_dbus_socket().await;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
spawn_signal_handler(shutdown_tx);
let mut last_err = None;
for attempt in 1..=5 {
let _root_path = parse_object_path(ROOT_PATH)?;
@@ -128,13 +126,16 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
inventory.drives().len(),
inventory.blocks().len(),
);
wait_for_shutdown().await?;
let _ = shutdown_rx.changed().await;
eprintln!("redbear-udisks: shutdown signal received, exiting cleanly");
drop(connection);
return Ok(());
}
Err(err) => {
if attempt < 5 {
eprintln!("redbear-udisks: attempt {attempt}/5 failed ({err}), retrying in 2s...");
eprintln!(
"redbear-udisks: attempt {attempt}/5 failed ({err}), retrying in 2s..."
);
tokio::time::sleep(Duration::from_secs(2)).await;
}
last_err = Some(err.into());
@@ -29,6 +29,8 @@ const DEVICE_STATE_DISCHARGING: u32 = 2;
const DEVICE_STATE_EMPTY: u32 = 3;
const DEVICE_STATE_FULLY_CHARGED: u32 = 4;
const POLL_INTERVAL_SECS: u64 = 30;
#[derive(Debug, Clone)]
struct PowerRuntime {
root: PathBuf,
@@ -59,20 +61,20 @@ enum DeviceDescriptor {
Battery(String),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
struct AdapterState {
native_path: String,
online: bool,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
struct BatteryState {
native_path: String,
state_bits: u64,
percentage: Option<f64>,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, PartialEq)]
struct PowerSnapshot {
adapters: Vec<AdapterState>,
batteries: Vec<BatteryState>,
@@ -328,31 +330,6 @@ impl PowerSnapshot {
}
}
#[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> {
@@ -368,7 +345,7 @@ impl UPowerDaemon {
String::from("0.1.0")
}
#[zbus(property(emits_changed_signal = "const"), name = "OnBattery")]
#[zbus(property(emits_changed_signal = "false"), name = "OnBattery")]
fn on_battery(&self) -> bool {
self.runtime.snapshot().on_battery()
}
@@ -476,6 +453,28 @@ impl PowerDevice {
}
}
fn spawn_signal_handler(shutdown_tx: tokio::sync::watch::Sender<bool>) {
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
tokio::select! {
_ = sigterm.recv() => {},
_ = tokio::signal::ctrl_c() => {},
}
} else {
let _ = tokio::signal::ctrl_c().await;
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
let _ = shutdown_tx.send(true);
});
}
async fn run_daemon() -> Result<(), Box<dyn Error>> {
wait_for_dbus_socket().await;
let runtime = PowerRuntime::discover()?;
@@ -486,6 +485,9 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
}
let _display_device_path = parse_object_path(DISPLAY_DEVICE_PATH)?;
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
spawn_signal_handler(shutdown_tx);
let mut last_err = None;
for attempt in 1..=5 {
let mut builder = system_connection_builder()?
@@ -527,13 +529,44 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
match builder.build().await {
Ok(connection) => {
eprintln!("redbear-upower: registered {BUS_NAME} on the system bus");
wait_for_shutdown().await?;
let upower_path = parse_object_path(UPOWER_PATH)?;
let signal_emitter = SignalEmitter::new(&connection, upower_path)?;
let mut last_snapshot = runtime.snapshot();
let mut poll_interval =
tokio::time::interval(Duration::from_secs(POLL_INTERVAL_SECS));
loop {
tokio::select! {
result = shutdown_rx.changed() => {
if result.is_err() {
eprintln!("redbear-upower: signal handler exited unexpectedly");
}
eprintln!("redbear-upower: shutdown signal received, exiting cleanly");
break;
}
_ = poll_interval.tick() => {
let current_snapshot = runtime.snapshot();
if current_snapshot != last_snapshot {
eprintln!(
"redbear-upower: power state changed, emitting Changed signal"
);
let _ = UPowerDaemon::changed(&signal_emitter).await;
last_snapshot = current_snapshot;
}
}
}
}
drop(connection);
return Ok(());
}
Err(err) => {
if attempt < 5 {
eprintln!("redbear-upower: attempt {attempt}/5 failed ({err}), retrying in 2s...");
eprintln!(
"redbear-upower: attempt {attempt}/5 failed ({err}), retrying in 2s..."
);
tokio::time::sleep(Duration::from_secs(2)).await;
}
last_err = Some(err.into());