sessiond: gate the blank actuator on logind LockedHint
This commit is contained in:
parent
29a3c472f5
commit
2c1ee8bc00
2 changed files with 40 additions and 3 deletions
|
|
@ -55,7 +55,7 @@ use tracing::{info, warn};
|
||||||
/// path, so monitoring the alias yields a subscription that is silently never
|
/// path, so monitoring the alias yields a subscription that is silently never
|
||||||
/// delivered. That is the failure the shell already hit
|
/// delivered. That is the failure the shell already hit
|
||||||
/// (`[session-events] no session path returned`).
|
/// (`[session-events] no session path returned`).
|
||||||
fn resolve_session_path() -> Result<String> {
|
pub fn resolve_session_path() -> Result<String> {
|
||||||
let uid = std::fs::metadata("/proc/self")
|
let uid = std::fs::metadata("/proc/self")
|
||||||
.map(|m| m.uid())
|
.map(|m| m.uid())
|
||||||
.context("reading our own uid")?;
|
.context("reading our own uid")?;
|
||||||
|
|
@ -121,7 +121,7 @@ fn resolve_session_path() -> Result<String> {
|
||||||
|
|
||||||
/// One-shot read, so the machine starts from the truth rather than from a
|
/// One-shot read, so the machine starts from the truth rather than from a
|
||||||
/// default that happens to be wrong until the first signal arrives.
|
/// default that happens to be wrong until the first signal arrives.
|
||||||
fn read_locked_hint(path: &str) -> Result<bool> {
|
pub fn read_locked_hint(path: &str) -> Result<bool> {
|
||||||
let out = Command::new("busctl")
|
let out = Command::new("busctl")
|
||||||
.args([
|
.args([
|
||||||
"--system",
|
"--system",
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,21 @@ fn spawn_lock_hint_watcher(shared: &Arc<Shared>) {
|
||||||
|
|
||||||
/// Carry out one decision. Failures are recorded in the forensic trail — a
|
/// Carry out one decision. Failures are recorded in the forensic trail — a
|
||||||
/// blank that did not happen is a state divergence, not a log line to lose.
|
/// blank that did not happen is a state divergence, not a log line to lose.
|
||||||
|
/// Ask logind whether this session is locked. Fail closed: if the question
|
||||||
|
/// cannot be answered, the answer is "not locked", because the failure this
|
||||||
|
/// guards is a dark screen on an open session.
|
||||||
|
fn session_is_locked() -> bool {
|
||||||
|
match crate::sessiond::lockhint::resolve_session_path()
|
||||||
|
.and_then(|p| crate::sessiond::lockhint::read_locked_hint(&p))
|
||||||
|
{
|
||||||
|
Ok(locked) => locked,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("could not read LockedHint ({e:#}); treating the session as unlocked");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn execute(shared: &Arc<Shared>, action: Action) {
|
fn execute(shared: &Arc<Shared>, action: Action) {
|
||||||
// The lock is not a shell command, so it does not go through the executor
|
// The lock is not a shell command, so it does not go through the executor
|
||||||
// table below. It is the authority acting as the authority: the shell owns
|
// table below. It is the authority acting as the authority: the shell owns
|
||||||
|
|
@ -265,7 +280,29 @@ fn execute(shared: &Arc<Shared>, action: Action) {
|
||||||
// dim is what pinned brightness at 10/255.
|
// dim is what pinned brightness at 10/255.
|
||||||
Action::Dim => ("brightnessctl", vec!["set", "10"], "panel-dim"),
|
Action::Dim => ("brightnessctl", vec!["set", "10"], "panel-dim"),
|
||||||
Action::Restore => unreachable!("handled above; the restore carries a value"),
|
Action::Restore => unreachable!("handled above; the restore carries a value"),
|
||||||
Action::Blank => (DPMS_EXECUTOR, vec!["off"], "panel-off"),
|
Action::Blank => {
|
||||||
|
// The invariant, enforced where it cannot be reasoned around: the
|
||||||
|
// panel does not go dark unless logind says this session is
|
||||||
|
// locked, right now. The machine's own `Locked` is a belief, and a
|
||||||
|
// belief that entered by one route (locked_ack) and can only leave
|
||||||
|
// by another (logind's falling edge) gets stuck — observed
|
||||||
|
// 2026-07-26: machine `locked`, LockedHint `no`, panel off, and
|
||||||
|
// double-tap waking straight to an unlocked screen.
|
||||||
|
//
|
||||||
|
// Checked at the actuator, not at the decision, because every path
|
||||||
|
// to a dark panel ends here and only here.
|
||||||
|
if !session_is_locked() {
|
||||||
|
warn!("refusing to blank: logind says this session is not locked");
|
||||||
|
shared.lock().device_state.record_error(
|
||||||
|
"device-state",
|
||||||
|
"panel-off",
|
||||||
|
"refused: session not locked per logind",
|
||||||
|
);
|
||||||
|
request_session_lock(shared);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(DPMS_EXECUTOR, vec!["off"], "panel-off")
|
||||||
|
}
|
||||||
Action::Lock => unreachable!("handled above; the lock is not a shell command"),
|
Action::Lock => unreachable!("handled above; the lock is not a shell command"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue