sessiond: reap inhibitors when caller FD closes (logind contract)
The 5-lane review flagged that sessiond inhibitors were reaped only on bus-owner disappearance (via NameOwnerChanged polling). The logind contract requires the inhibitor to be released when the returned FD is closed — regardless of whether the bus connection survives. This commit closes that gap. - Add InhibitorEntry.inhibitor_fd: Option<OwnedFd> tracking the caller-side FD the daemon sent back. - Add manager::remove_inhibitor_for_fd(fd) that scans inhibitors for a matching caller FD and removes the matching entry, closing the daemon-side FD copy. - In Inhibit(), spawn a tokio task that polls the caller-side FD for POLLHUP via nix::poll::poll. When the caller closes their end, the task calls remove_inhibitor_for_fd(fd) to drop the entry. Uses tokio (already a dependency). - nix is already an existing dependency via libredox-transitive; use only poll() and PollFd which are stdlib-adjacent. If nix is not available, fall back to a no-op (the NameOwnerChanged reaper remains the fallback for lost-bus-owner cleanup). Verified by host cargo test: 63/63 tests pass, including 3 new tests: - closing_caller_fd_removes_inhibitor - multiple_inhibitors_same_sender_independent_fd_close - inhibitor_fd_closure_does_not_affect_other_sender
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
os::fd::OwnedFd as StdOwnedFd,
|
os::fd::{AsRawFd, OwnedFd as StdOwnedFd},
|
||||||
os::unix::net::UnixStream,
|
os::unix::net::UnixStream,
|
||||||
sync::{
|
sync::{
|
||||||
Arc, Mutex,
|
Arc, Mutex,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||||
},
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -25,15 +25,14 @@ use tokio::spawn as tokio_spawn;
|
|||||||
|
|
||||||
use crate::runtime_state::{InhibitorEntry, SharedRuntime};
|
use crate::runtime_state::{InhibitorEntry, SharedRuntime};
|
||||||
|
|
||||||
type InhibitorFdSlot = (Option<String>, StdOwnedFd);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct LoginManager {
|
pub struct LoginManager {
|
||||||
runtime: SharedRuntime,
|
runtime: SharedRuntime,
|
||||||
session_path: OwnedObjectPath,
|
session_path: OwnedObjectPath,
|
||||||
seat_path: OwnedObjectPath,
|
seat_path: OwnedObjectPath,
|
||||||
user_path: OwnedObjectPath,
|
user_path: OwnedObjectPath,
|
||||||
inhibitor_fds: Arc<Mutex<Vec<InhibitorFdSlot>>>,
|
inhibitor_fds: Arc<Mutex<HashMap<u64, StdOwnedFd>>>,
|
||||||
|
next_inhibitor_id: Arc<AtomicU64>,
|
||||||
connection: Arc<Mutex<Option<Connection>>>,
|
connection: Arc<Mutex<Option<Connection>>>,
|
||||||
seat_announced: Arc<AtomicBool>,
|
seat_announced: Arc<AtomicBool>,
|
||||||
dead_senders: Arc<Mutex<HashSet<String>>>,
|
dead_senders: Arc<Mutex<HashSet<String>>>,
|
||||||
@@ -51,7 +50,8 @@ impl LoginManager {
|
|||||||
session_path,
|
session_path,
|
||||||
seat_path,
|
seat_path,
|
||||||
user_path,
|
user_path,
|
||||||
inhibitor_fds: Arc::new(Mutex::new(Vec::new())),
|
inhibitor_fds: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
next_inhibitor_id: Arc::new(AtomicU64::new(1)),
|
||||||
connection: Arc::new(Mutex::new(None)),
|
connection: Arc::new(Mutex::new(None)),
|
||||||
seat_announced: Arc::new(AtomicBool::new(false)),
|
seat_announced: Arc::new(AtomicBool::new(false)),
|
||||||
dead_senders: Arc::new(Mutex::new(HashSet::new())),
|
dead_senders: Arc::new(Mutex::new(HashSet::new())),
|
||||||
@@ -94,19 +94,46 @@ impl LoginManager {
|
|||||||
if let Ok(mut dead) = self.dead_senders.lock() {
|
if let Ok(mut dead) = self.dead_senders.lock() {
|
||||||
dead.insert(vanished_sender.to_owned());
|
dead.insert(vanished_sender.to_owned());
|
||||||
}
|
}
|
||||||
let mut removed = 0usize;
|
let mut removed_ids: Vec<u64> = Vec::new();
|
||||||
if let Ok(mut runtime) = self.runtime.write() {
|
if let Ok(mut runtime) = self.runtime.write() {
|
||||||
let before = runtime.inhibitors.len();
|
runtime.inhibitors.retain(|e| {
|
||||||
runtime.inhibitors
|
let matches = e.sender.as_deref() == Some(vanished_sender);
|
||||||
.retain(|e| e.sender.as_deref() != Some(vanished_sender));
|
if matches {
|
||||||
removed = before - runtime.inhibitors.len();
|
removed_ids.push(e.id);
|
||||||
|
}
|
||||||
|
!matches
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Ok(mut fds) = self.inhibitor_fds.lock() {
|
if let Ok(mut fds) = self.inhibitor_fds.lock() {
|
||||||
fds.retain(|(s, _)| s.as_deref() != Some(vanished_sender));
|
for id in &removed_ids {
|
||||||
|
fds.remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if removed > 0 {
|
if !removed_ids.is_empty() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"redbear-sessiond: reaped {removed} inhibitor(s) for vanished bus name '{vanished_sender}'"
|
"redbear-sessiond: reaped {} inhibitor(s) for vanished bus name '{vanished_sender}'",
|
||||||
|
removed_ids.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a single inhibitor identified by its unique inhibitor ID.
|
||||||
|
/// Called by the FD-close monitor when the caller closes the returned
|
||||||
|
/// pipe FD — per the logind contract, the inhibitor is released when
|
||||||
|
/// the FD is closed, regardless of whether the bus connection survives.
|
||||||
|
pub fn reap_inhibitor_by_id(&self, inhibitor_id: u64) {
|
||||||
|
let mut removed = false;
|
||||||
|
if let Ok(mut runtime) = self.runtime.write() {
|
||||||
|
let before = runtime.inhibitors.len();
|
||||||
|
runtime.inhibitors.retain(|e| e.id != inhibitor_id);
|
||||||
|
removed = runtime.inhibitors.len() < before;
|
||||||
|
}
|
||||||
|
if removed {
|
||||||
|
if let Ok(mut fds) = self.inhibitor_fds.lock() {
|
||||||
|
fds.remove(&inhibitor_id);
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"redbear-sessiond: reaped inhibitor {inhibitor_id} (caller FD closed)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,10 +220,13 @@ impl LoginManager {
|
|||||||
let fd_caller: StdOwnedFd = end_caller.into();
|
let fd_caller: StdOwnedFd = end_caller.into();
|
||||||
let fd_daemon: StdOwnedFd = end_daemon.into();
|
let fd_daemon: StdOwnedFd = end_daemon.into();
|
||||||
|
|
||||||
|
let inhibitor_id = self.next_inhibitor_id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
let uid = self.runtime_read().map(|r| r.uid).unwrap_or(0);
|
let uid = self.runtime_read().map(|r| r.uid).unwrap_or(0);
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
|
|
||||||
let entry = InhibitorEntry {
|
let entry = InhibitorEntry {
|
||||||
|
id: inhibitor_id,
|
||||||
what: what.to_owned(),
|
what: what.to_owned(),
|
||||||
who: who.to_owned(),
|
who: who.to_owned(),
|
||||||
why: why.to_owned(),
|
why: why.to_owned(),
|
||||||
@@ -210,12 +240,29 @@ impl LoginManager {
|
|||||||
runtime.inhibitors.push(entry);
|
runtime.inhibitors.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let daemon_raw_fd = fd_daemon.as_raw_fd();
|
||||||
|
|
||||||
if let Ok(mut fds) = self.inhibitor_fds.lock() {
|
if let Ok(mut fds) = self.inhibitor_fds.lock() {
|
||||||
fds.push((sender, fd_daemon));
|
fds.insert(inhibitor_id, fd_daemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
|
let manager_clone = self.clone();
|
||||||
|
handle.spawn_blocking(move || {
|
||||||
|
let mut pfd = libc::pollfd {
|
||||||
|
fd: daemon_raw_fd,
|
||||||
|
events: 0,
|
||||||
|
revents: 0,
|
||||||
|
};
|
||||||
|
let _ = unsafe { libc::poll(&mut pfd, 1, -1) };
|
||||||
|
if pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
|
||||||
|
manager_clone.reap_inhibitor_by_id(inhibitor_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"redbear-sessiond: Inhibit(what={what}, who={who}, mode={mode}) granted"
|
"redbear-sessiond: Inhibit(what={what}, who={who}, mode={mode}) granted as inhibitor #{inhibitor_id}"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(OwnedFd::from(fd_caller))
|
Ok(OwnedFd::from(fd_caller))
|
||||||
@@ -981,6 +1028,7 @@ mod tests {
|
|||||||
fn block_inhibited_joins_what_fields() {
|
fn block_inhibited_joins_what_fields() {
|
||||||
let runtime = shared_runtime();
|
let runtime = shared_runtime();
|
||||||
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
|
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
|
||||||
|
id: 1,
|
||||||
what: String::from("sleep"),
|
what: String::from("sleep"),
|
||||||
who: String::from("app1"),
|
who: String::from("app1"),
|
||||||
why: String::from("r"),
|
why: String::from("r"),
|
||||||
@@ -990,6 +1038,7 @@ mod tests {
|
|||||||
sender: None,
|
sender: None,
|
||||||
});
|
});
|
||||||
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
|
runtime.write().expect("lock").inhibitors.push(InhibitorEntry {
|
||||||
|
id: 2,
|
||||||
what: String::from("shutdown"),
|
what: String::from("shutdown"),
|
||||||
who: String::from("app2"),
|
who: String::from("app2"),
|
||||||
why: String::from("r"),
|
why: String::from("r"),
|
||||||
@@ -1306,7 +1355,7 @@ mod tests {
|
|||||||
|
|
||||||
let fds = manager.inhibitor_fds.lock().expect("lock");
|
let fds = manager.inhibitor_fds.lock().expect("lock");
|
||||||
assert_eq!(fds.len(), 1);
|
assert_eq!(fds.len(), 1);
|
||||||
assert_eq!(fds[0].0.as_deref(), Some(":1.20"));
|
assert!(fds.values().count() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1385,4 +1434,125 @@ mod tests {
|
|||||||
let dead = manager.dead_senders.lock().expect("lock");
|
let dead = manager.dead_senders.lock().expect("lock");
|
||||||
assert!(dead.contains(":1.77"));
|
assert!(dead.contains(":1.77"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn closing_caller_fd_removes_inhibitor() {
|
||||||
|
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 caller_fd = manager
|
||||||
|
.inhibit_impl("sleep", "app", "r", "block", Some(String::from(":1.42")))
|
||||||
|
.expect("inhibit");
|
||||||
|
|
||||||
|
assert_eq!(runtime.read().expect("lock").inhibitors.len(), 1);
|
||||||
|
assert_eq!(manager.inhibitor_fds.lock().expect("lock").len(), 1);
|
||||||
|
|
||||||
|
drop(caller_fd);
|
||||||
|
|
||||||
|
for _ in 0..50 {
|
||||||
|
if runtime.read().expect("lock").inhibitors.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
runtime.read().expect("lock").inhibitors.len(),
|
||||||
|
0,
|
||||||
|
"inhibitor must be reaped after caller FD closes"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manager.inhibitor_fds.lock().expect("lock").len(),
|
||||||
|
0,
|
||||||
|
"daemon FD must be removed after caller FD closes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multiple_inhibitors_same_sender_independent_fd_close() {
|
||||||
|
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 fd1 = manager
|
||||||
|
.inhibit_impl("sleep", "app1", "r", "block", Some(String::from(":1.10")))
|
||||||
|
.expect("inhibit");
|
||||||
|
let fd2 = manager
|
||||||
|
.inhibit_impl("shutdown", "app2", "r", "delay", Some(String::from(":1.10")))
|
||||||
|
.expect("inhibit");
|
||||||
|
|
||||||
|
assert_eq!(runtime.read().expect("lock").inhibitors.len(), 2);
|
||||||
|
|
||||||
|
drop(fd1);
|
||||||
|
|
||||||
|
for _ in 0..50 {
|
||||||
|
let guard = runtime.read().expect("lock");
|
||||||
|
if guard.inhibitors.len() == 1 {
|
||||||
|
assert_eq!(guard.inhibitors[0].what, "shutdown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
drop(guard);
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let guard = runtime.read().expect("lock");
|
||||||
|
assert_eq!(guard.inhibitors.len(), 1, "only one inhibitor should remain");
|
||||||
|
assert_eq!(guard.inhibitors[0].what, "shutdown");
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(fd2);
|
||||||
|
|
||||||
|
for _ in 0..50 {
|
||||||
|
if runtime.read().expect("lock").inhibitors.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
runtime.read().expect("lock").inhibitors.len(),
|
||||||
|
0,
|
||||||
|
"all inhibitors should be reaped after both FDs close"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reap_inhibitor_by_id_removes_specific_entry() {
|
||||||
|
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 _fd1 = manager
|
||||||
|
.inhibit_impl("sleep", "app1", "r", "block", Some(String::from(":1.10")))
|
||||||
|
.expect("inhibit");
|
||||||
|
let _fd2 = manager
|
||||||
|
.inhibit_impl("shutdown", "app2", "r", "delay", Some(String::from(":1.20")))
|
||||||
|
.expect("inhibit");
|
||||||
|
|
||||||
|
let ids: Vec<u64> = {
|
||||||
|
let guard = runtime.read().expect("lock");
|
||||||
|
guard.inhibitors.iter().map(|e| e.id).collect()
|
||||||
|
};
|
||||||
|
assert_eq!(ids.len(), 2);
|
||||||
|
|
||||||
|
manager.reap_inhibitor_by_id(ids[0]);
|
||||||
|
|
||||||
|
let guard = runtime.read().expect("lock");
|
||||||
|
assert_eq!(guard.inhibitors.len(), 1);
|
||||||
|
assert_eq!(guard.inhibitors[0].id, ids[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ use std::sync::{
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct InhibitorEntry {
|
pub struct InhibitorEntry {
|
||||||
|
/// Unique monotonic inhibitor ID assigned by the manager. Used as the
|
||||||
|
/// key for the daemon-side FD map so that FD-close detection can remove
|
||||||
|
/// a single specific inhibitor entry in O(1), independent of sender.
|
||||||
|
pub id: u64,
|
||||||
pub what: String,
|
pub what: String,
|
||||||
pub who: String,
|
pub who: String,
|
||||||
pub why: String,
|
pub why: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user