Watch
1
0
Fork
You've already forked souveraine
0

lock: adopt the session lock across a scene reload

sessiond admits shell_ready from the lease holder's own pid; the shell
carries its lock request through the reload so WlSessionLock adopts the
existing lock instead of taking the unlock branch on it.
This commit is contained in:
Fimeg 2026-07-31 14:32:29 -04:00
commit efe2d0d1e0
2 changed files with 211 additions and 1 deletions

View file

@ -66,6 +66,10 @@ Scope {
target: GlobalStates
function onScreenLockedChanged() {
Persistent.states.lock.locked = GlobalStates.screenLocked;
// Persistent is a file and answers asynchronously, so it can
// never be the thing a reload reads at construction. This is
// the same fact held in-process, where the reload can see it.
lockContinuity.held = GlobalStates.screenLocked;
if (GlobalStates.screenLocked) {
lockContext.reset();
lockContext.tryFingerUnlock();
@ -101,8 +105,54 @@ Scope {
}
}
// TASK-48. The lock REQUEST, carried across a quickshell scene reload by
// quickshell's own reload machinery — in-process and synchronous, which is
// what this has to be. Declared before the WlSessionLock on purpose:
// `Scope` is a ReloadPropagator and reloads its children in declaration
// order, so `held` is restored and applied before the lock below reloads.
//
// Why any of this is needed, from reading quickshell's session_lock.cpp:
// on reload the new WlSessionLock adopts the outgoing one's
// SessionLockManager and then calls realizeLockTarget() with whatever
// `locked` evaluated to at construction. GlobalStates is a fresh singleton
// by then, so screenLocked is false, so lockTarget is false — and the
// false branch is `unlock()`, which on an adopted manager that IS locked
// sends ext_session_lock_v1.unlock_and_destroy. A scene reload therefore
// UNLOCKS the session. What is left is a compositor with no lock surface,
// a phone that reads black, and `{"locked":false,"lockRequested":true}`.
// If instead the adoption does not match, the new manager's lock() is
// refused (the outgoing lock is still the process-global holder) and
// updateSurfaces(true) is called anyway — that is the FATAL this task was
// raised for. Same root cause, two faces.
//
// With the request true at construction, realizeLockTarget takes the adopt
// branch: surfaces are re-created against the SAME compositor lock,
// manager->lock() declines harmlessly because we already hold it, and
// updateSurfaces sees an active lock. No re-request, no denial, no
// unlock_and_destroy. This is the task's "adopting beats re-requesting",
// and it is the only branch that never opens the panel.
PersistentProperties {
id: lockContinuity
reloadableId: "souveraineLockContinuity"
property bool held: false
onLoaded: {
if (lockContinuity.held && !GlobalStates.screenLocked) {
console.log("[lock] scene reload owed a lock — adopting, not re-requesting");
GlobalStates.screenLocked = true;
}
}
}
WlSessionLock {
id: lock
// Explicit so the adoption survives being reparented out of a Scope.
// Under a Scope children match by index and this is unused; anywhere
// else an empty reloadableId means oldInstance is ALWAYS null, the
// manager is never adopted, and the lock cannot be re-acquired for the
// life of the process.
reloadableId: "souveraineSessionLock"
locked: GlobalStates.screenLocked
surface: root.sessionLockSurface
@ -155,6 +205,25 @@ Scope {
}
}
// `secure` is an EDGE, and after a reload that adopts the lock that edge
// is in the past: the compositor acked before this tree existed, so
// onSecureChanged never fires and GlobalStates.screenLockSecure would sit
// false on a session that is genuinely secure. Everything gating on it —
// redaction, capability tiers, and the locked_ack this handoff owes
// sessiond — would then be wrong in the dangerous direction.
//
// Publish the LEVEL once the reload has actually run. Component.onCompleted
// is too early: Reloadable defers onReload to after component completion,
// so `lock.secure` is still false there. Setting screenLockSecure is enough
// to send the ack — SessiondBridge is already listening for it.
Connections {
target: Quickshell
function onReloadCompleted() {
GlobalStates.screenLockSecure = lock.secure;
root.dismissBloomIfSecure();
}
}
// Idempotent, and deliberately NOT a timeout. A bloom that outlives its
// reason is a bug to locate, not something to paper over with a timer — if
// this is still up while the lock is secure, the caller is missing and the