Wake shell status from service and hardware events
This commit is contained in:
1 parent
8bb0b1da9c
commit
570984a39b
9 files changed
+380
-98
No files matched your search
@@ -49,6 +49,10 @@ impl Cache {
|
||||
self.value.clone()
|
||||
}
|
||||
|
||||
pub(super) fn next_retry(&self) -> Option<Instant> {
|
||||
(!self.signals.alive() || self.value.is_none()).then_some(self.retry_at)
|
||||
}
|
||||
|
||||
pub(super) fn changed(&self) -> bool {
|
||||
self.signals.revision() != self.revision
|
||||
}
|
||||
@@ -62,7 +66,7 @@ mod tests {
|
||||
|
||||
fn cache() -> Cache {
|
||||
Cache {
|
||||
signals: Signals::test(Arc::new((Mutex::new((0, false, None, false)), Condvar::new()))),
|
||||
signals: Signals::test(Arc::new((Mutex::new((0, false, None, false, 0)), Condvar::new()))),
|
||||
revision: 0,
|
||||
action: u64::MAX,
|
||||
value: None,
|
||||
|
||||
@@ -23,8 +23,7 @@ impl State {
|
||||
pub(super) fn changed(&self) {
|
||||
self.revision.fetch_add(1, Ordering::AcqRel);
|
||||
// Synchronize with the poller's predicate check and condvar wait.
|
||||
let _guard = self.wake.0.lock().unwrap_or_else(|error| error.into_inner());
|
||||
self.wake.1.notify_one();
|
||||
super::super::wake_status(&self.wake);
|
||||
}
|
||||
|
||||
pub(super) fn set_alive(&self, alive: bool) {
|
||||
@@ -324,7 +323,7 @@ context.objects = [
|
||||
.stderr(Stdio::null());
|
||||
let mut server = Server(command.spawn().unwrap());
|
||||
wait_for(|| root.path().join("pipewire-0").exists());
|
||||
let wake = Arc::new((Mutex::new((0, false, None, false)), Condvar::new()));
|
||||
let wake = Arc::new((Mutex::new((0, false, None, false, 0)), Condvar::new()));
|
||||
let signals = Signals::start_remote(wake, Some(root.path().join("pipewire-0").to_str().unwrap().into()));
|
||||
wait_for(|| signals.alive());
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
@@ -7,7 +7,7 @@ use cosmic::iced::futures::channel::oneshot;
|
||||
use cosmic::iced::futures::{FutureExt, StreamExt};
|
||||
use zbus::blocking::{Connection, MessageIterator};
|
||||
|
||||
use super::StatusBus;
|
||||
use super::{PollState, StatusBus, wake_status};
|
||||
|
||||
struct Signals {
|
||||
revision: Arc<AtomicU64>,
|
||||
@@ -16,7 +16,7 @@ struct Signals {
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
fn new(connection: &Connection, service: &str) -> zbus::Result<Self> {
|
||||
fn new(connection: &Connection, service: &str, wake: Option<Arc<PollState>>) -> zbus::Result<Self> {
|
||||
// Install both rules before the snapshot, including when the service is
|
||||
// currently absent. Owner changes invalidate paths after a restart.
|
||||
let changes = MessageIterator::for_match_rule(
|
||||
@@ -51,6 +51,7 @@ impl Signals {
|
||||
// this before entering our async runtime.
|
||||
let changes = changes.into_inner();
|
||||
let owners = owners.into_inner();
|
||||
let event_wake = wake.clone();
|
||||
runtime.block_on(async move {
|
||||
let mut events = cosmic::iced::futures::stream::select(changes, owners);
|
||||
let mut stopped = stopped.fuse();
|
||||
@@ -59,6 +60,7 @@ impl Signals {
|
||||
_ = stopped => break,
|
||||
event = events.next() => {
|
||||
changed.fetch_add(1, Ordering::Release);
|
||||
if let Some(wake) = &event_wake { wake_status(wake); }
|
||||
if !matches!(event, Some(Ok(_))) { break; }
|
||||
}
|
||||
}
|
||||
@@ -69,6 +71,9 @@ impl Signals {
|
||||
});
|
||||
}
|
||||
running.store(false, Ordering::Release);
|
||||
if let Some(wake) = &wake {
|
||||
wake_status(wake);
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
revision,
|
||||
@@ -86,6 +91,8 @@ pub(super) struct Cache<T> {
|
||||
action: u64,
|
||||
value: Option<T>,
|
||||
retry_at: Instant,
|
||||
retry: bool,
|
||||
wake: Option<Arc<PollState>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> Cache<T> {
|
||||
@@ -98,33 +105,59 @@ impl<T: Clone> Cache<T> {
|
||||
action: u64::MAX,
|
||||
value: None,
|
||||
retry_at: Instant::now(),
|
||||
retry: false,
|
||||
wake: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_wake(service: &'static str, wake: Arc<PollState>) -> Self {
|
||||
let mut cache = Self::new(service);
|
||||
cache.wake = Some(wake);
|
||||
cache
|
||||
}
|
||||
|
||||
pub(super) fn next_retry(&self) -> Option<Instant> {
|
||||
(self.action != u64::MAX && (self.signals.is_none() || self.retry)).then_some(self.retry_at)
|
||||
}
|
||||
|
||||
pub(super) fn read(&mut self, action: u64, read: fn(&Connection) -> zbus::Result<Option<T>>) -> Option<T> {
|
||||
if self.signals.as_ref().is_some_and(|s| !s.alive.load(Ordering::Acquire)) {
|
||||
self.signals = None;
|
||||
self.bus.connection = None;
|
||||
self.value = None;
|
||||
self.retry_at = Instant::now();
|
||||
}
|
||||
if self.signals.is_none() {
|
||||
self.signals = self.bus.query(|connection| Signals::new(connection, self.service));
|
||||
if self.signals.is_none() && Instant::now() >= self.retry_at {
|
||||
self.signals = self
|
||||
.bus
|
||||
.query(|connection| Signals::new(connection, self.service, self.wake.clone()));
|
||||
self.revision = 0;
|
||||
}
|
||||
// If subscription setup fails, read fresh rather than trusting a cache
|
||||
// with no invalidation. Retry setup on the next regular status poll.
|
||||
// with no invalidation. Retry setup at the bounded recovery deadline.
|
||||
let revision = self.signals.as_ref().map(|s| s.revision.load(Ordering::Acquire));
|
||||
if revision.is_none()
|
||||
|| revision != Some(self.revision)
|
||||
if (revision.is_none() && Instant::now() >= self.retry_at)
|
||||
|| revision.is_some_and(|revision| revision != self.revision)
|
||||
|| action != self.action
|
||||
|| (self.value.is_none() && Instant::now() >= self.retry_at)
|
||||
|| (self.retry && Instant::now() >= self.retry_at)
|
||||
{
|
||||
// Keep signal subscriptions across service errors. A dead bus is
|
||||
// detected by the listener; a transient query failure retries later.
|
||||
self.value = self
|
||||
.bus
|
||||
.query(|connection| Ok(connection.clone()))
|
||||
.and_then(|connection| read(&connection).ok().flatten());
|
||||
let result = self.bus.query(|connection| {
|
||||
let owned = connection
|
||||
.call_method(
|
||||
Some("org.freedesktop.DBus"),
|
||||
"/org/freedesktop/DBus",
|
||||
Some("org.freedesktop.DBus"),
|
||||
"NameHasOwner",
|
||||
&(self.service,),
|
||||
)?
|
||||
.body()
|
||||
.deserialize::<bool>()?;
|
||||
if owned { read(connection) } else { Ok(None) }
|
||||
});
|
||||
self.retry = result.is_none();
|
||||
self.value = result.flatten();
|
||||
self.retry_at = Instant::now() + Duration::from_secs(5);
|
||||
self.action = action;
|
||||
self.revision = revision.unwrap_or(0);
|
||||
@@ -178,6 +211,7 @@ mod tests {
|
||||
assert_eq!(cache.read(0, read_counter), Some(1));
|
||||
}
|
||||
assert_eq!(count.load(Ordering::Relaxed), 1);
|
||||
assert!(cache.next_retry().is_none(), "healthy subscriptions must not poll");
|
||||
assert_eq!(cache.read(1, read_counter), Some(2));
|
||||
server
|
||||
.emit_signal(None::<&str>, "/test", "org.ferese.StatusCounter", "Changed", &())
|
||||
@@ -193,6 +227,7 @@ mod tests {
|
||||
assert!(Instant::now() < deadline, "owner loss kept stale data");
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
assert!(cache.next_retry().is_none(), "absent services wait for owner changes");
|
||||
server.request_name("org.ferese.StatusCounter").unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while cache.read(1, read_counter).is_none() {
|
||||
@@ -207,6 +242,9 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
assert!(cache.read(2, read_counter).is_none());
|
||||
// The production cache reconnects to the system bus after an error;
|
||||
// keep this recovery attempt on the isolated test bus instead.
|
||||
cache.bus.connection = Some(bus.connect());
|
||||
cache.retry_at = Instant::now();
|
||||
assert!(cache.read(2, read_counter).is_some());
|
||||
}
|
||||
@@ -218,7 +256,11 @@ mod tests {
|
||||
let client = bus.connect();
|
||||
let server = bus.connect();
|
||||
server.request_name("org.ferese.StatusTest").unwrap();
|
||||
let signals = Signals::new(&client, "org.ferese.StatusTest").unwrap();
|
||||
let wake = Arc::new((
|
||||
std::sync::Mutex::new((0, false, None, false, 0)),
|
||||
std::sync::Condvar::new(),
|
||||
));
|
||||
let signals = Signals::new(&client, "org.ferese.StatusTest", Some(wake.clone())).unwrap();
|
||||
let wait = |before| {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while signals.revision.load(Ordering::Acquire) == before && Instant::now() < deadline {
|
||||
@@ -230,6 +272,13 @@ mod tests {
|
||||
server
|
||||
.emit_signal(None::<&str>, "/test", "org.ferese.StatusTest", "Changed", &())
|
||||
.unwrap();
|
||||
let (state, timeout) = wake
|
||||
.1
|
||||
.wait_timeout_while(wake.0.lock().unwrap(), Duration::from_secs(2), |state| state.4 == 0)
|
||||
.unwrap();
|
||||
assert!(!timeout.timed_out(), "service signal must wake the status worker");
|
||||
assert!(state.4 > 0);
|
||||
drop(state);
|
||||
wait(before);
|
||||
let before = signals.revision.load(Ordering::Acquire);
|
||||
server.release_name("org.ferese.StatusTest").unwrap();
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Kernel battery/backlight events, with a cancellation fd and no polling timer.
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use super::{PollState, wake_status};
|
||||
|
||||
pub(super) struct Monitor {
|
||||
_stop: UnixStream,
|
||||
alive: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
fn relevant(message: &[u8]) -> bool {
|
||||
message
|
||||
.split(|byte| *byte == 0)
|
||||
.any(|field| field == b"SUBSYSTEM=backlight" || field == b"SUBSYSTEM=power_supply")
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
pub fn alive(&self) -> bool {
|
||||
self.alive.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn start(wake: Arc<PollState>) -> std::io::Result<Self> {
|
||||
// SAFETY: socket has no borrowed pointer arguments; successful fd is owned below.
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_NETLINK,
|
||||
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
|
||||
libc::NETLINK_KOBJECT_UEVENT,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: the successful socket call returned a unique owned descriptor.
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
// SAFETY: zero is a valid initialization for sockaddr_nl including padding.
|
||||
let mut address: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||
address.nl_family = libc::AF_NETLINK as u16;
|
||||
address.nl_groups = 1;
|
||||
// SAFETY: the address pointer and its length describe the initialized struct.
|
||||
if unsafe {
|
||||
libc::bind(
|
||||
fd.as_raw_fd(),
|
||||
(&raw const address).cast(),
|
||||
std::mem::size_of_val(&address) as _,
|
||||
)
|
||||
} < 0
|
||||
{
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
let (stop, cancelled) = UnixStream::pair()?;
|
||||
let alive = Arc::new(AtomicBool::new(true));
|
||||
let running = alive.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("ferese-hardware-events".into())
|
||||
.spawn(move || {
|
||||
let mut descriptors = [
|
||||
libc::pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
},
|
||||
libc::pollfd {
|
||||
fd: cancelled.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
},
|
||||
];
|
||||
let mut buffer = [0u8; 65536];
|
||||
loop {
|
||||
// SAFETY: both descriptors remain owned by this thread; poll writes within the array.
|
||||
let result = unsafe { libc::poll(descriptors.as_mut_ptr(), 2, -1) };
|
||||
if result < 0 && std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
if result < 0
|
||||
|| descriptors[1].revents != 0
|
||||
|| descriptors[0].revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0
|
||||
{
|
||||
break;
|
||||
}
|
||||
let mut length = std::mem::size_of_val(&address) as libc::socklen_t;
|
||||
// SAFETY: the owned fd, writable byte buffer and sockaddr storage are valid for their lengths.
|
||||
let count = unsafe {
|
||||
libc::recvfrom(
|
||||
fd.as_raw_fd(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len(),
|
||||
0,
|
||||
(&raw mut address).cast(),
|
||||
&mut length,
|
||||
)
|
||||
};
|
||||
if count < 0 {
|
||||
break;
|
||||
}
|
||||
if address.nl_pid == 0 && relevant(&buffer[..count as usize]) {
|
||||
wake_status(&wake);
|
||||
}
|
||||
}
|
||||
running.store(false, Ordering::Release);
|
||||
wake_status(&wake);
|
||||
})?;
|
||||
Ok(Self { _stop: stop, alive })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn only_battery_and_backlight_events_refresh_status() {
|
||||
assert!(relevant(b"change@/devices/panel\0SUBSYSTEM=backlight\0"));
|
||||
assert!(relevant(b"SUBSYSTEM=power_supply\0ACTION=change\0"));
|
||||
assert!(!relevant(b"SUBSYSTEM=input\0"));
|
||||
assert!(!relevant(b"OTHER=SUBSYSTEM=backlight\0"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user