Watch
1
0
Fork
You've already forked souveraine
0

sessiond: create lock surfaces before the verdict, not after it

`ensure_surfaces` returned early unless `state.locked`, and `locked` is
what the compositor sends once lock frames are up. ext-session-lock-v1 is
explicit: "The locked event must not be sent until a new 'locked' frame
has been presented on all outputs." So a compositor honouring the spec
waits for the surfaces this function refused to create until it had heard
from the compositor. Neither side can move.

Hyprland hides it by acking `locked` before any lock surface exists,
which is why this stood. Measured against viewtop on blueline today:
sessiond sat in `blocking_dispatch` waiting for a verdict that could not
arrive, and the session crash-looped every eleven seconds.

The wait loop was also deaf to the control channel for its whole 60s
budget, so a shell registering during it got `lock session did not
release in time` (server.rs waits 5s) and then asked the compositor for
a lock this thread still held — TASK-48's crash, reached from the daemon
side. It now drains Release before each dispatch and draws while it
waits, which is the order the protocol describes.

408 tests pass.
This commit is contained in:
Fimeg 2026-08-02 11:24:46 -04:00
commit 5edf0c410e

View file

@ -221,6 +221,32 @@ pub fn run(rx: Receiver<Msg>, wake_read: OwnedFd) -> Result<SessionOutcome> {
if std::time::Instant::now() >= verdict_deadline {
bail!("compositor never acknowledged the session lock (60s)");
}
// Draw *while* waiting, not after. A spec-honouring compositor sends
// `locked` only once a lock frame is on every output, so the surfaces
// have to exist and be painted before the verdict can arrive — see
// `ensure_surfaces`. Doing this after the wait deadlocks against
// exactly the compositor the protocol describes.
ensure_surfaces(&mut state, &qh);
if state.dirty {
redraw_all(&mut state, &qh)?;
state.dirty = false;
}
// The handoff must not wait out the verdict. This loop used to be deaf
// to the control channel for its whole 60s budget, so a shell that
// registered during it got `lock session did not release in time`
// (server.rs waits 5s) and then asked the compositor for a lock this
// thread was still holding — TASK-48's crash, from the daemon side.
while let Ok(msg) = rx.try_recv() {
match msg {
Msg::Release => {
info!("releasing session lock to shell (before the verdict)");
return Ok(SessionOutcome::Released);
}
}
}
queue.blocking_dispatch(&mut state).context("waiting for locked")?;
}
if state.finished {
@ -301,9 +327,20 @@ pub fn run(rx: Receiver<Msg>, wake_read: OwnedFd) -> Result<SessionOutcome> {
}
fn ensure_surfaces(state: &mut LockState, qh: &QueueHandle<LockState>) {
if !state.locked {
return;
}
// Gated on the lock *object*, not on the `locked` event, and the
// difference is a deadlock.
//
// ext-session-lock-v1 is explicit: "The locked event must not be sent
// until a new 'locked' frame has been presented on all outputs." So a
// compositor that honours the spec is waiting for exactly the surfaces
// this function refuses to create until it has heard from the compositor.
// Neither side can move.
//
// Hyprland hides it by acking `locked` before any lock surface exists,
// which is why this stood for months. Measured against viewtop on blueline
// 2026-08-02: sessiond sat in `blocking_dispatch` waiting for a verdict
// that could not arrive, the shell's `shell_ready` handoff timed out
// behind it, and the session crash-looped every eleven seconds.
let (Some(compositor), Some(lock)) = (&state.compositor, &state.lock) else {
return;
};