7a5b5963c4
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.
1193 lines
41 KiB
Rust
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);
|
|
}
|
|
}
|