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.
This commit is contained in:
alvarosth
2026-10-04 22:17:06 -04:00
committed by GitHub
parent 30dc1122c5
commit 4bd3e091fe
3 changed files with 74 additions and 30 deletions
+16 -1
View File
@@ -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
+34 -16
View File
@@ -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) {
+24 -13
View File
@@ -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() {