fix(media-controls): stop re-asking login1 for an inhibitor every 2 s

`SleepInhibitor::acquire` runs on every Playing state update and retried
the `Inhibit` call each time it failed. On a headless daemon with no
logind session polkit answers `InteractiveAuthorizationRequired` forever,
which produced one WARN every ~2 s (369 lines in 11 min on the Pi smoke,
2026-08-29). Classify the failure: a policy denial is logged once at info
and never retried for this process; a transient error backs off 5 min.
Playback was never affected.

cc
This commit is contained in:
vicrodh committed 2026-08-29 18:35:03 -06:00
1 parent e90ea8cada
commit a9c886b0d3
1 file changed
+97 -4
+97 -4
View File
@@ -13,11 +13,48 @@
//! unlike MPRIS (session bus), so this keeps its own lazy connection.
use mpris_server::zbus::{self, zvariant::OwnedFd};
use std::time::{Duration, Instant};
const LOGIN1_DEST: &str = "org.freedesktop.login1";
const LOGIN1_PATH: &str = "/org/freedesktop/login1";
const LOGIN1_IFACE: &str = "org.freedesktop.login1.Manager";
/// After a transient `Inhibit` failure, wait this long before the next try.
/// `set_playing(true)` arrives with every playback-state update (~2 s), so
/// without a backoff a headless box logged the same warning every 2 s (369
/// lines in 11 min on a Pi daemon, 2026-08-29).
const RETRY_BACKOFF: Duration = Duration::from_secs(300);
/// Why the last `Inhibit` attempt did not produce a lock.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Denial {
/// A permanent policy answer for this process (polkit wants an
/// interactive auth we can never give — a daemon with no logind
/// session). Never retried.
Policy,
/// Anything else (bus hiccup, logind restarting). Retried after
/// `RETRY_BACKOFF`.
Transient,
}
/// Pure retry decision, so the backoff can be unit-tested without a bus.
fn should_attempt(last: Option<(Denial, Instant)>, now: Instant) -> bool {
match last {
None => true,
Some((Denial::Policy, _)) => false,
Some((Denial::Transient, at)) => now.duration_since(at) >= RETRY_BACKOFF,
}
}
fn classify(error: &zbus::Error) -> Denial {
let text = error.to_string();
if text.contains("InteractiveAuthorizationRequired") || text.contains("AccessDenied") {
Denial::Policy
} else {
Denial::Transient
}
}
/// Holds the inhibitor fd while playing. Owned by the MPRIS update loop
/// (single-threaded); acquire/release are driven by playback-state updates.
pub struct SleepInhibitor {
@@ -27,11 +64,18 @@ pub struct SleepInhibitor {
/// The inhibitor lock. `Some` while Playing; dropping it closes the fd,
/// which is how logind releases the lock.
fd: Option<OwnedFd>,
/// Last failed attempt and why — gates the next attempt (see
/// `should_attempt`).
last_failure: Option<(Denial, Instant)>,
}
impl SleepInhibitor {
pub fn new() -> Self {
Self { conn: None, fd: None }
Self {
conn: None,
fd: None,
last_failure: None,
}
}
/// Drive the inhibitor from a playback transition: acquire on Playing,
@@ -50,6 +94,9 @@ impl SleepInhibitor {
if self.fd.is_some() {
return;
}
if !should_attempt(self.last_failure, Instant::now()) {
return;
}
let conn = match &self.conn {
Some(c) => c.clone(),
None => match zbus::Connection::system().await {
@@ -58,7 +105,11 @@ impl SleepInhibitor {
c
}
Err(e) => {
log::warn!("[inhibit] system bus unavailable, cannot inhibit sleep: {e}");
log::warn!(
"[inhibit] system bus unavailable, cannot inhibit sleep (retry in {}s): {e}",
RETRY_BACKOFF.as_secs()
);
self.last_failure = Some((Denial::Transient, Instant::now()));
return;
}
},
@@ -77,10 +128,27 @@ impl SleepInhibitor {
Ok(fd) => {
log::info!("[inhibit] acquired login1 sleep:idle inhibitor");
self.fd = Some(fd);
self.last_failure = None;
}
Err(e) => {
log::warn!("[inhibit] bad Inhibit reply (expected fd): {e}");
self.last_failure = Some((Denial::Transient, Instant::now()));
}
Err(e) => log::warn!("[inhibit] bad Inhibit reply (expected fd): {e}"),
},
Err(e) => log::warn!("[inhibit] login1 Inhibit call failed: {e}"),
Err(e) => {
let denial = classify(&e);
match denial {
Denial::Policy => log::info!(
"[inhibit] login1 refuses a sleep:idle inhibitor for this process ({e}); \
giving up — playback is unaffected"
),
Denial::Transient => log::warn!(
"[inhibit] login1 Inhibit call failed (retry in {}s): {e}",
RETRY_BACKOFF.as_secs()
),
}
self.last_failure = Some((denial, Instant::now()));
}
}
}
@@ -91,3 +159,28 @@ impl SleepInhibitor {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_attempt_is_always_made() {
assert!(should_attempt(None, Instant::now()));
}
#[test]
fn policy_denial_is_never_retried() {
let at = Instant::now();
assert!(!should_attempt(Some((Denial::Policy, at)), at));
assert!(!should_attempt(Some((Denial::Policy, at)), at + RETRY_BACKOFF * 10));
}
#[test]
fn transient_failure_waits_out_the_backoff() {
let at = Instant::now();
assert!(!should_attempt(Some((Denial::Transient, at)), at));
assert!(!should_attempt(Some((Denial::Transient, at)), at + RETRY_BACKOFF / 2));
assert!(should_attempt(Some((Denial::Transient, at)), at + RETRY_BACKOFF));
}
}