diff --git a/src/sessiond/lock.rs b/src/sessiond/lock.rs index 26f9111..5d1631a 100644 --- a/src/sessiond/lock.rs +++ b/src/sessiond/lock.rs @@ -221,6 +221,32 @@ pub fn run(rx: Receiver, wake_read: OwnedFd) -> Result { 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, wake_read: OwnedFd) -> Result { } fn ensure_surfaces(state: &mut LockState, qh: &QueueHandle) { - 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; };