Files
RedBear-OS/local/recipes/system/redbear-sessiond/source/src/manager.rs
T
vasilito 7a5b5963c4 round 16: log R15 entries in SUPERSEDED-DOC-LOG + document sessiond sleep TODO
Round 16 audit follow-up. Two small but useful additions:

1. local/docs/SUPERSEDED-DOC-LOG.md — appended R15 entries:
   greeter/netctl/hotplugd let-underscore-to-logged (5682072e58),
   acmd setrens security fix (883e8147ec), README null+8 contradiction
   reconciliation, and the redbear-live.iso stale-reference fix.

2. local/recipes/system/redbear-sessiond/source/src/manager.rs —
   the can_hibernate / can_hybrid_sleep / can_suspend_then_hibernate
   / can_sleep functions all return hardcoded 'na'. Documented the
   integration plan in a block comment above them: when the
   /scheme/sys/sleep and /scheme/sys/hybrid_sleep schemes exist
   in the Redox kernel, replace these with fs::metadata() probes
   matching the kstop_writable() pattern used by
   can_power_off/can_reboot/can_suspend. No code change yet —
   just the docstring pointing the next maintainer at the pattern.

Without this comment, a future maintainer reading the four
hardcoded 'na' returns would not know about the kstop_writable
probe pattern that enables the matching sleep capabilities.

2 files changed, +32/-5.
2026-07-27 22:03:19 +09:00

1193 lines
41 KiB
Rust

use std::{
collections::HashSet,
fs,
io::Write,
os::fd::OwnedFd as StdOwnedFd,
os::unix::net::UnixStream,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use zbus::{
Connection,
fdo::{self, DBusProxy},
interface,
message::Header,
names::BusName,
object_server::SignalEmitter,
zvariant::{OwnedFd, OwnedObjectPath},
};
use tokio::spawn as tokio_spawn;
use crate::runtime_state::{InhibitorEntry, SharedRuntime};
type InhibitorFdSlot = (Option<String>, StdOwnedFd);
#[derive(Clone, Debug)]
pub struct LoginManager {
runtime: SharedRuntime,
session_path: OwnedObjectPath,
seat_path: OwnedObjectPath,
user_path: OwnedObjectPath,
inhibitor_fds: Arc<Mutex<Vec<InhibitorFdSlot>>>,
connection: Arc<Mutex<Option<Connection>>>,
seat_announced: Arc<AtomicBool>,
dead_senders: Arc<Mutex<HashSet<String>>>,
}
impl LoginManager {
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())),
connection: Arc::new(Mutex::new(None)),
seat_announced: Arc::new(AtomicBool::new(false)),
dead_senders: Arc::new(Mutex::new(HashSet::new())),
}
}
pub fn set_connection(&self, connection: Connection) {
if let Ok(mut guard) = self.connection.lock() {
*guard = Some(connection);
}
let self_clone = self.clone();
tokio_spawn(async move { self_clone.announce_seat_if_needed().await });
let self_clone = self.clone();
tokio_spawn(async move { self_clone.run_inhibitor_reaper().await });
}
pub async fn announce_seat_if_needed(&self) {
if self.seat_announced.swap(true, Ordering::AcqRel) {
return;
}
let seat_id = match self.runtime.read() {
Ok(runtime) => runtime.seat_id.clone(),
Err(_) => String::from("seat0"),
};
self.emit_seat_new(&seat_id, &self.seat_path).await;
}
fn get_connection(&self) -> Option<Connection> {
self.connection
.lock()
.ok()
.and_then(|g| g.as_ref().cloned())
}
/// Remove all inhibitors whose `sender` matches `vanished_sender`,
/// and drop the corresponding daemon-side pipe FDs so the pipe
/// breaks and the caller (if still alive) observes EOF.
pub fn reap_inhibitors_for_sender(&self, vanished_sender: &str) {
if let Ok(mut dead) = self.dead_senders.lock() {
dead.insert(vanaged_sender.to_owned());
}
let mut removed = 0usize;
if let Ok(mut runtime) = self.runtime.write() {
let before = runtime.inhibitors.len();
runtime.inhibitors
.retain(|e| e.sender.as_deref() != Some(vanished_sender));
removed = before - runtime.inhibitors.len();
}
if let Ok(mut fds) = self.inhibitor_fds.lock() {
fds.retain(|(s, _)| s.as_deref() != Some(vanished_sender));
}
if removed > 0 {
eprintln!(
"redbear-sessiond: reaped {removed} inhibitor(s) for vanished bus name '{vanished_sender}'"
);
}
}
/// Background task: periodically asks the D-Bus daemon whether each
/// tracked sender still owns its unique name. When `name_has_owner`
/// returns `false`, the sender has disconnected and its inhibitors
/// are reaped.
///
/// Uses `org.freedesktop.DBus.NameHasOwner` polling instead of a
/// `NameOwnerChanged` signal subscription because consuming zbus
/// signal streams requires a `StreamExt` that is not available
/// without adding a direct dependency on `futures-lite` /
/// `futures-util`. The 2-second poll interval keeps reaping
/// responsive while staying lightweight.
pub async fn run_inhibitor_reaper(&self) {
let conn = match self.get_connection() {
Some(c) => c,
None => {
eprintln!(
"redbear-sessiond: inhibitor reaper has no connection; inhibitor lifecycle reaping disabled"
);
return;
}
};
let proxy = match DBusProxy::new(&conn).await {
Ok(p) => p,
Err(err) => {
eprintln!(
"redbear-sessiond: inhibitor reaper failed to create DBus proxy: {err}"
);
return;
}
};
let mut interval = tokio::time::interval(Duration::from_secs(2));
interval.tick().await;
loop {
interval.tick().await;
let senders: Vec<String> = match self.runtime.read() {
Ok(runtime) => runtime
.inhibitors
.iter()
.filter_map(|e| e.sender.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect(),
Err(_) => continue,
};
for sender in &senders {
let name = match BusName::try_from(sender.as_str()) {
Ok(n) => n,
Err(_) => continue,
};
match proxy.name_has_owner(name).await {
Ok(true) => {}
Ok(false) => {
self.reap_inhibitors_for_sender(sender);
}
Err(err) => {
eprintln!(
"redbear-sessiond: inhibitor reaper name_has_owner('{sender}') failed: {err}"
);
}
}
}
}
}
async fn emit_prepare_for_shutdown(&self, before: bool) {
if let Some(conn) = self.get_connection() {
if let Err(err) = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"PrepareForShutdown",
&before,
)
.await
{
eprintln!(
"redbear-sessiond: PrepareForShutdown(before={before}) emit failed: {err}"
);
}
}
}
async fn emit_prepare_for_sleep(&self, before: bool) {
if let Some(conn) = self.get_connection() {
if let Err(err) = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"PrepareForSleep",
&before,
)
.await
{
eprintln!(
"redbear-sessiond: PrepareForSleep(before={before}) emit failed: {err}"
);
}
}
}
pub async fn emit_seat_new(&self, id: &str, seat_path: &OwnedObjectPath) {
if let Some(conn) = self.get_connection() {
if let Err(err) = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"SeatNew",
&(id, seat_path),
)
.await
{
eprintln!(
"redbear-sessiond: SeatNew({id}, {seat_path}) emit failed: {err}"
);
} else {
eprintln!("redbear-sessiond: SeatNew emitted for {id} -> {seat_path}");
}
}
}
pub async fn emit_seat_removed(&self, id: &str, seat_path: &OwnedObjectPath) {
if let Some(conn) = self.get_connection() {
if let Err(err) = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"SeatRemoved",
&(id, seat_path),
)
.await
{
eprintln!(
"redbear-sessiond: SeatRemoved({id}, {seat_path}) emit failed: {err}"
);
} else {
eprintln!("redbear-sessiond: SeatRemoved emitted for {id} -> {seat_path}");
}
}
}
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")))
}
fn runtime_write(&self) -> fdo::Result<std::sync::RwLockWriteGuard<'_, crate::runtime_state::SessionRuntime>> {
self.runtime
.write()
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))
}
fn session_matches(runtime: &crate::runtime_state::SessionRuntime, session_id: &str) -> bool {
session_id == runtime.session_id || session_id == "auto"
}
fn user_matches(runtime: &crate::runtime_state::SessionRuntime, uid: u32) -> bool {
uid == runtime.uid
}
}
fn kstop_writable() -> bool {
// metadata() rather than an actual write — writing "shutdown"/"reset"
// to /scheme/sys/kstop would trigger the action.
fs::metadata("/scheme/sys/kstop").is_ok()
}
#[interface(name = "org.freedesktop.login1.Manager")]
impl LoginManager {
fn get_session(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
let runtime = self.runtime_read()?;
if id == runtime.session_id || id == "auto" {
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)>> {
let runtime = self.runtime_read()?;
Ok(vec![(
runtime.session_id.clone(),
runtime.uid,
runtime.username.clone(),
runtime.seat_id.clone(),
self.session_path.clone(),
)])
}
fn get_seat(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
let runtime = self.runtime_read()?;
if id == runtime.seat_id {
return Ok(self.seat_path.clone());
}
Err(fdo::Error::Failed(format!("unknown login1 seat '{id}'")))
}
fn get_user(&self, uid: u32) -> fdo::Result<OwnedObjectPath> {
let runtime = self.runtime_read()?;
if Self::user_matches(&runtime, uid) {
return Ok(self.user_path.clone());
}
Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}")))
}
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> {
if kstop_writable() {
Ok(String::from("yes"))
} else {
Ok(String::from("na"))
}
}
fn can_reboot(&self) -> fdo::Result<String> {
if kstop_writable() {
Ok(String::from("yes"))
} else {
Ok(String::from("na"))
}
}
fn can_suspend(&self) -> fdo::Result<String> {
if kstop_writable() {
Ok(String::from("yes"))
} else {
Ok(String::from("na"))
}
}
// Hardcoded "na" until /scheme/sys/sleep + /scheme/sys/hybrid_sleep
// land; when they do, mirror the kstop_writable() probe pattern
// used by can_power_off/can_reboot/can_suspend so the functions
// auto-enable.
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"))
}
async 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;
}
self.emit_prepare_for_shutdown(true).await;
match fs::OpenOptions::new().write(true).open("/scheme/sys/kstop") {
Ok(mut f) => {
if let Err(e) = f.write_all(b"shutdown") {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = false;
}
eprintln!("redbear-sessiond: PowerOff kstop write failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot write to /scheme/sys/kstop for shutdown: {e}"
)));
}
}
Err(e) => {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = false;
}
eprintln!("redbear-sessiond: PowerOff kstop open failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot open /scheme/sys/kstop for shutdown: {e}"
)));
}
}
self.emit_prepare_for_shutdown(false).await;
Ok(())
}
async fn reboot(&self, _interactive: bool) -> fdo::Result<()> {
eprintln!("redbear-sessiond: Reboot requested");
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = true;
}
self.emit_prepare_for_shutdown(true).await;
match fs::OpenOptions::new().write(true).open("/scheme/sys/kstop") {
Ok(mut f) => {
if let Err(e) = f.write_all(b"reset") {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = false;
}
eprintln!("redbear-sessiond: Reboot kstop write failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot write to /scheme/sys/kstop for reboot: {e}"
)));
}
}
Err(e) => {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_shutdown = false;
}
eprintln!("redbear-sessiond: Reboot kstop open failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot open /scheme/sys/kstop for reboot: {e}"
)));
}
}
self.emit_prepare_for_shutdown(false).await;
Ok(())
}
async fn suspend(&self, _interactive: bool) -> fdo::Result<()> {
eprintln!("redbear-sessiond: Suspend requested — sending to kernel kstop");
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_sleep = true;
}
self.emit_prepare_for_sleep(true).await;
match fs::OpenOptions::new().write(true).open("/scheme/sys/kstop") {
Ok(mut f) => {
if let Err(e) = f.write_all(b"s3") {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_sleep = false;
}
eprintln!("redbear-sessiond: Suspend kstop write failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot write to /scheme/sys/kstop for suspend: {e}"
)));
}
}
Err(e) => {
if let Ok(mut runtime) = self.runtime.write() {
runtime.preparing_for_sleep = false;
}
eprintln!("redbear-sessiond: Suspend kstop open failed: {e}");
return Err(fdo::Error::Failed(format!(
"cannot open /scheme/sys/kstop for suspend: {e}"
)));
}
}
self.emit_prepare_for_sleep(false).await;
Ok(())
}
fn get_session_by_pid(&self, _pid: u32) -> fdo::Result<OwnedObjectPath> {
Ok(self.session_path.clone())
}
fn get_user_by_pid(&self, _pid: u32) -> fdo::Result<OwnedObjectPath> {
Ok(self.user_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<()> {
let mut runtime = self.runtime_write()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
runtime.active = true;
eprintln!("redbear-sessiond: ActivateSession({session_id})");
Ok(())
}
fn activate_session_on_seat(&self, session_id: &str, seat_id: &str) -> fdo::Result<()> {
let mut runtime = self.runtime_write()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
if seat_id != runtime.seat_id {
return Err(fdo::Error::Failed(format!("unknown login1 seat '{seat_id}'")));
}
runtime.active = true;
eprintln!("redbear-sessiond: ActivateSessionOnSeat({session_id}, {seat_id})");
Ok(())
}
fn lock_session(&self, session_id: &str) -> fdo::Result<()> {
let mut runtime = self.runtime_write()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
runtime.locked_hint = true;
eprintln!("redbear-sessiond: LockSession({session_id})");
Ok(())
}
fn unlock_session(&self, session_id: &str) -> fdo::Result<()> {
let mut runtime = self.runtime_write()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
runtime.locked_hint = false;
eprintln!("redbear-sessiond: UnlockSession({session_id})");
Ok(())
}
fn lock_sessions(&self) -> fdo::Result<()> {
let session_id = {
let mut runtime = self.runtime_write()?;
runtime.locked_hint = true;
runtime.session_id.clone()
};
eprintln!("redbear-sessiond: LockSessions() -> {session_id}");
Ok(())
}
fn unlock_sessions(&self) -> fdo::Result<()> {
let session_id = {
let mut runtime = self.runtime_write()?;
runtime.locked_hint = false;
runtime.session_id.clone()
};
eprintln!("redbear-sessiond: UnlockSessions() -> {session_id}");
Ok(())
}
fn terminate_session(&self, session_id: &str) -> fdo::Result<()> {
let mut runtime = self.runtime_write()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
runtime.state = String::from("closing");
runtime.active = false;
eprintln!("redbear-sessiond: TerminateSession({session_id})");
Ok(())
}
fn terminate_user(&self, uid: u32) -> fdo::Result<()> {
let mut runtime = self.runtime_write()?;
if !Self::user_matches(&runtime, uid) {
return Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}")));
}
runtime.state = String::from("closing");
runtime.active = false;
eprintln!("redbear-sessiond: TerminateUser({uid})");
Ok(())
}
fn kill_session(&self, session_id: &str, who: &str, signal_number: i32) -> fdo::Result<()> {
let runtime = self.runtime_read()?;
if !Self::session_matches(&runtime, session_id) {
return Err(fdo::Error::Failed(format!("unknown login1 session '{session_id}'")));
}
let leader = runtime.leader;
eprintln!(
"redbear-sessiond: KillSession({session_id}, who={who}, signal={signal_number}) — sending to PID {leader}"
);
drop(runtime);
let sig = signal_number as u32;
// Use libc::kill so the binary links on the host (libredox's
// redox_kill_v1 / redox_strerror_v1 are only present on Redox).
let result = unsafe {
libc::kill(leader as libc::pid_t, sig as libc::c_int)
};
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {err}"
)));
}
Ok(())
}
fn kill_user(&self, uid: u32, signal_number: i32) -> fdo::Result<()> {
let runtime = self.runtime_read()?;
if !Self::user_matches(&runtime, uid) {
return Err(fdo::Error::Failed(format!("unknown login1 user uid {uid}")));
}
let leader = runtime.leader;
eprintln!(
"redbear-sessiond: KillUser({uid}, signal={signal_number}) — sending to PID {leader}"
);
drop(runtime);
let sig = signal_number as u32;
// See kill_session for the libc::kill rationale.
let result = unsafe {
libc::kill(leader as libc::pid_t, sig as libc::c_int)
};
if result != 0 {
let err = std::io::Error::last_os_error();
return Err(fdo::Error::Failed(format!(
"kill PID {leader} with signal {sig}: {err}"
)));
}
Ok(())
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleHint")]
fn idle_hint(&self) -> bool {
self.runtime_read().map(|r| r.idle_hint).unwrap_or(false)
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleSinceHint")]
fn idle_since_hint(&self) -> u64 {
self.runtime_read()
.map(|r| r.last_activity_us.load(Ordering::Relaxed))
.unwrap_or(0)
}
#[zbus(property(emits_changed_signal = "const"), name = "IdleSinceHintMonotonic")]
fn idle_since_hint_monotonic(&self) -> u64 {
self.runtime_read()
.map(|r| r.last_activity_us.load(Ordering::Relaxed))
.unwrap_or(0)
}
#[zbus(property(emits_changed_signal = "const"), name = "BlockInhibited")]
fn block_inhibited(&self) -> String {
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 {
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")]
fn inhibit_delay_max_usec(&self) -> u64 {
self.runtime_read()
.map(|r| r.inhibit_delay_max_us.load(Ordering::Relaxed))
.unwrap_or(0)
}
#[zbus(property(emits_changed_signal = "const"), name = "HandleLidSwitch")]
fn handle_lid_switch(&self) -> String {
self.runtime_read()
.and_then(|r| {
r.handle_lid_switch
.read()
.map(|g| g.clone())
.map_err(|_| fdo::Error::Failed(String::from("login1 lid switch lock poisoned")))
})
.unwrap_or_else(|_| String::from("ignore"))
}
#[zbus(property(emits_changed_signal = "const"), name = "HandlePowerKey")]
fn handle_power_key(&self) -> String {
self.runtime_read()
.and_then(|r| {
r.handle_power_key
.read()
.map(|g| g.clone())
.map_err(|_| fdo::Error::Failed(String::from("login1 power key lock poisoned")))
})
.unwrap_or_else(|_| String::from("poweroff"))
}
#[zbus(property(emits_changed_signal = "const"), name = "PreparingForSleep")]
fn preparing_for_sleep(&self) -> bool {
self.runtime_read()
.map(|runtime| runtime.preparing_for_sleep)
.unwrap_or(false)
}
#[zbus(property(emits_changed_signal = "const"), name = "PreparingForShutdown")]
fn preparing_for_shutdown(&self) -> bool {
self.runtime_read()
.map(|runtime| runtime.preparing_for_shutdown)
.unwrap_or(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<()>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime_state::shared_runtime;
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")
.expect("runtime session id should resolve");
assert_eq!(path.as_str(), "/org/freedesktop/login1/session/c1");
}
#[test]
fn get_session_accepts_auto_alias() {
let manager = test_manager();
let path = manager
.get_session("auto")
.expect("auto alias should resolve to current session");
assert_eq!(path.as_str(), "/org/freedesktop/login1/session/c1");
}
#[test]
fn preparing_for_shutdown_reflects_runtime_state() {
let runtime = shared_runtime();
runtime
.write()
.expect("runtime lock should be writable")
.preparing_for_shutdown = true;
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"),
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();
// kstop_writable() probes /scheme/sys/kstop, which only exists on
// the Redox target. On a Linux host (where `cargo test` runs for
// host-runnable unit tests), the path is absent so the Can*
// methods correctly return "na". Mirror that runtime detection
// here so the test passes on both environments.
let expected = if kstop_writable() { "yes" } else { "na" };
assert_eq!(manager.can_power_off().unwrap(), expected);
assert_eq!(manager.can_reboot().unwrap(), expected);
assert_eq!(manager.can_suspend().unwrap(), expected);
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"));
}
#[test]
fn get_user_accepts_runtime_uid() {
let runtime = shared_runtime();
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 path = manager.get_user(1000).expect("runtime uid should resolve");
assert_eq!(path.as_str(), "/org/freedesktop/login1/user/current");
}
#[test]
fn lock_sessions_updates_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(),
);
manager.lock_sessions().expect("lock_sessions should succeed");
assert!(runtime.read().expect("lock").locked_hint);
manager.unlock_sessions().expect("unlock_sessions should succeed");
assert!(!runtime.read().expect("lock").locked_hint);
}
#[test]
fn activate_session_on_seat_rejects_unknown_seat() {
let manager = test_manager();
let err = manager
.activate_session_on_seat("c1", "seat9")
.expect_err("unknown seat should fail");
match err {
fdo::Error::Failed(message) => assert!(message.contains("seat9")),
other => panic!("expected Failed error, got {other:?}"),
}
}
#[test]
fn idle_since_hint_reads_last_activity_default_zero() {
let manager = test_manager();
assert_eq!(manager.idle_since_hint(), 0);
}
#[test]
fn idle_since_hint_reflects_runtime_update() {
let runtime = shared_runtime();
runtime
.read()
.expect("lock")
.last_activity_us
.store(123_456_789, Ordering::Relaxed);
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,
);
assert_eq!(manager.idle_since_hint(), 123_456_789);
assert_eq!(manager.idle_since_hint_monotonic(), 123_456_789);
}
#[test]
fn inhibit_delay_max_usec_reads_default() {
let manager = test_manager();
assert_eq!(manager.inhibit_delay_max_usec(), 5_000_000);
}
#[test]
fn inhibit_delay_max_usec_reflects_runtime_update() {
let runtime = shared_runtime();
runtime
.read()
.expect("lock")
.inhibit_delay_max_us
.store(10_000_000, Ordering::Relaxed);
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,
);
assert_eq!(manager.inhibit_delay_max_usec(), 10_000_000);
}
#[test]
fn handle_lid_switch_reads_default() {
let manager = test_manager();
assert_eq!(manager.handle_lid_switch(), "ignore");
}
#[test]
fn handle_lid_switch_reflects_runtime_update() {
let runtime = shared_runtime();
{
let guard = runtime.read().expect("lock");
let mut lid = guard.handle_lid_switch.write().expect("lock");
*lid = String::from("suspend");
}
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,
);
assert_eq!(manager.handle_lid_switch(), "suspend");
}
#[test]
fn handle_power_key_reads_default() {
let manager = test_manager();
assert_eq!(manager.handle_power_key(), "poweroff");
}
#[test]
fn handle_power_key_reflects_runtime_update() {
let runtime = shared_runtime();
{
let guard = runtime.read().expect("lock");
let mut pwr = guard.handle_power_key.write().expect("lock");
*pwr = String::from("suspend");
}
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,
);
assert_eq!(manager.handle_power_key(), "suspend");
}
#[tokio::test]
async fn power_off_returns_error_and_resets_flag_when_kstop_missing() {
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 result = manager.power_off(false).await;
assert!(result.is_err(), "power_off should fail without /scheme/sys/kstop");
let guard = runtime.read().expect("lock");
assert!(
!guard.preparing_for_shutdown,
"preparing_for_shutdown must be reset on kstop failure"
);
}
#[tokio::test]
async fn suspend_returns_error_and_resets_flag_when_kstop_missing() {
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 result = manager.suspend(false).await;
assert!(result.is_err(), "suspend should fail without /scheme/sys/kstop");
let guard = runtime.read().expect("lock");
assert!(
!guard.preparing_for_sleep,
"preparing_for_sleep must be reset on kstop failure"
);
}
#[test]
fn set_connection_stores_connection_slot() {
let manager = test_manager();
assert!(manager.get_connection().is_none());
}
#[test]
fn clone_preserves_atomic_values() {
let runtime = shared_runtime();
runtime
.read()
.expect("lock")
.inhibit_delay_max_us
.store(42, Ordering::Relaxed);
let snapshot = runtime.read().expect("lock").clone();
assert_eq!(snapshot.inhibit_delay_max_us.load(Ordering::Relaxed), 42);
}
}