From 4bd3e091fe21b47f4a237f616dcf55dbacba604f Mon Sep 17 00:00:00 2001 From: alvarosth <65863679+alvarosth@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:17:06 +0200 Subject: [PATCH] lock: take the lock back when the shell restarts into a locked session (#3685) * lock: take the lock back when the shell restarts into a locked session Check the state once when the module completes, on locked or lockedHint, and handle it the way onSessionLocked does, including the wait while the session is inactive. After a logind Unlock that the shell ignored for a lock it started, only lockedHint still says locked; and the module loads asynchronously, so the state can arrive before its Connections exist. * loginctl: read LockedHint from logind on unlock logind's Unlock signal does not clear LockedHint, only the locker does. Forcing it false told a shell restarted in that window that the session was no longer locked. --- core/internal/server/loginctl/monitor.go | 17 ++++++- core/internal/server/loginctl/monitor_test.go | 50 +++++++++++++------ quickshell/Modules/Lock/Lock.qml | 37 +++++++++----- 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/core/internal/server/loginctl/monitor.go b/core/internal/server/loginctl/monitor.go index c6bdbd02f..116a73b2a 100644 --- a/core/internal/server/loginctl/monitor.go +++ b/core/internal/server/loginctl/monitor.go @@ -1,6 +1,7 @@ package loginctl import ( + "context" "time" "github.com/AvengeMedia/dankgo/dbusutil" @@ -29,9 +30,10 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) { } case dbusSessionInterface + ".Unlock": + lockedHint := m.sessionLockedHint() m.stateMutex.Lock() m.state.Locked = false - m.state.LockedHint = false + m.state.LockedHint = lockedHint m.stateMutex.Unlock() m.notifySubscribers() m.prelockedReady.Store(false) @@ -109,6 +111,19 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) { } } +// logind's Unlock does not clear LockedHint, the locker does. Report logind's +// value so a shell restarted while still locked can take the lock back. +func (m *Manager) sessionLockedHint() bool { + if m.sessionObj == nil { + return false + } + props, err := m.getSessionProperties(context.Background()) + if err != nil { + return false + } + return dbusutil.GetOr(props, "LockedHint", false) +} + func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) { if len(sig.Body) < 2 { return diff --git a/core/internal/server/loginctl/monitor_test.go b/core/internal/server/loginctl/monitor_test.go index 8b2a72cab..4633290cf 100644 --- a/core/internal/server/loginctl/monitor_test.go +++ b/core/internal/server/loginctl/monitor_test.go @@ -6,8 +6,10 @@ import ( "testing" "time" + mockdbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -34,25 +36,41 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) { } func TestManager_HandleDBusSignal_Unlock(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Locked: true, - LockedHint: true, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), + tests := []struct { + name string + lockedHint bool + }{ + {name: "logind cleared LockedHint", lockedHint: false}, + {name: "logind still reports LockedHint", lockedHint: true}, } - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Session.Unlock", + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sessionObj := mockdbus.NewMockBusObject(t) + sessionObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.login1.Session").Return(&dbus.Call{ + Body: []any{map[string]dbus.Variant{"LockedHint": dbus.MakeVariant(tt.lockedHint)}}, + }) + + manager := &Manager{ + state: &SessionState{ + Locked: true, + LockedHint: true, + }, + stateMutex: sync.RWMutex{}, + dirty: make(chan struct{}, 1), + sessionObj: sessionObj, + } + + manager.handleDBusSignal(&dbus.Signal{ + Name: "org.freedesktop.login1.Session.Unlock", + }) + + manager.stateMutex.RLock() + defer manager.stateMutex.RUnlock() + assert.False(t, manager.state.Locked) + assert.Equal(t, tt.lockedHint, manager.state.LockedHint) + }) } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.Locked) - assert.False(t, manager.state.LockedHint) } func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) { diff --git a/quickshell/Modules/Lock/Lock.qml b/quickshell/Modules/Lock/Lock.qml index d8c051dae..b873aa9d2 100644 --- a/quickshell/Modules/Lock/Lock.qml +++ b/quickshell/Modules/Lock/Lock.qml @@ -79,8 +79,15 @@ Scope { Component.onCompleted: { IdleService.lockComponent = this; IdleService.isSessionLockSecure = sessionLock.secure; - if (SettingsData.lockAtStartup && !freshGreeterLogin()) + if (SettingsData.lockAtStartup && !freshGreeterLogin()) { lock(); + return; + } + // A shell restarted into a locked session: after a logind Unlock + // ignored for a lock it started, only lockedHint still says locked, + // and the state can arrive before this module's Connections exist. + if (SessionService.locked || SessionService.lockedHint) + followLoginctlLock(); } function notifyLockedHint(locked: bool) { @@ -116,6 +123,21 @@ Scope { return true; } + function followLoginctlLock() { + if (shouldLock || pendingLock) + return; + if (handleLoginctlCustomLock()) + return; + if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) { + pendingLock = true; + lockInitiatedLocally = false; + return; + } + lockInitiatedLocally = false; + lockPowerOffArmed = powerOffOnLock; + shouldLock = true; + } + function resetLockRetry() { lockRetryAttempts = 0; lockRetryPending = false; @@ -193,18 +215,7 @@ Scope { target: SessionService function onSessionLocked() { - if (shouldLock || pendingLock) - return; - if (handleLoginctlCustomLock()) - return; - if (!SessionService.active && SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration) { - pendingLock = true; - lockInitiatedLocally = false; - return; - } - lockInitiatedLocally = false; - lockPowerOffArmed = powerOffOnLock; - shouldLock = true; + root.followLoginctlLock(); } function onSessionUnlocked() {