1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

screensaver: emit ActiveChanged on lock/unlock (#1761)

This commit is contained in:
Patrick Fischer
2026-02-21 00:05:30 +08:00
committed by GitHub
parent ecd6d70da6
commit 8ad0cf8e5f
6 changed files with 297 additions and 67 deletions

View File

@@ -16,4 +16,8 @@ const (
dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
)

View File

@@ -191,6 +191,12 @@ func (m *Manager) Close() {
return true
})
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
close(ch)
m.screensaverSubscribers.Delete(key)
return true
})
if m.systemConn != nil {
m.systemConn.Close()
}

View File

@@ -1,6 +1,7 @@
package freedesktop
import (
"fmt"
"path/filepath"
"strings"
"sync/atomic"
@@ -15,45 +16,9 @@ type screensaverHandler struct {
manager *Manager
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
if err != nil {
log.Warnf("Failed to request screensaver name: %v", err)
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name already owned by another process")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
handler := &screensaverHandler{manager: m}
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
return nil
}
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
return nil
}
screensaverIface := introspect.Interface{
Name: dbusScreensaverInterface,
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
return introspect.Interface{
Name: ifaceName,
Methods: []introspect.Method{
{
Name: "Inhibit",
@@ -69,40 +34,110 @@ func (m *Manager) initializeScreensaver() error {
{Name: "cookie", Type: "u", Direction: "in"},
},
},
{
Name: "GetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "out"},
},
},
{
Name: "SetActive",
Args: []introspect.Arg{
{Name: "active", Type: "b", Direction: "in"},
},
},
{
Name: "Lock",
},
},
Signals: []introspect.Signal{
{
Name: "ActiveChanged",
Args: []introspect.Arg{
{Name: "new_value", Type: "b"},
},
},
},
}
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
screensaverIface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
handler := &screensaverHandler{manager: m}
// Try to claim org.freedesktop.ScreenSaver (may fail if the compositor
// or another process already owns this name).
if reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue); err != nil {
log.Warnf("Failed to request screensaver name %s: %v", dbusScreensaverName, err)
} else if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name %s already owned by another process", dbusScreensaverName)
} else if err := m.exportScreensaverOnPaths(handler, dbusScreensaverInterface,
dbusScreensaverPath, dbusScreensaverPath2); err != nil {
log.Warnf("Failed to export freedesktop screensaver: %v", err)
} else {
m.screensaverFreedesktopClaimed = true
log.Infof("Claimed %s on session bus", dbusScreensaverName)
}
introNode2 := &introspect.Node{
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
screensaverIface,
},
// Try to claim org.gnome.ScreenSaver independently as a fallback.
if reply, err := m.sessionConn.RequestName(dbusGnomeScreensaverName, dbus.NameFlagDoNotQueue); err != nil {
log.Warnf("Failed to request screensaver name %s: %v", dbusGnomeScreensaverName, err)
} else if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name %s already owned by another process", dbusGnomeScreensaverName)
} else if err := m.exportScreensaverOnPaths(handler, dbusGnomeScreensaverInterface,
dbusGnomeScreensaverPath); err != nil {
log.Warnf("Failed to export gnome screensaver: %v", err)
} else {
m.screensaverGnomeClaimed = true
log.Infof("Claimed %s on session bus", dbusGnomeScreensaverName)
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
log.Warn("No screensaver interface could be claimed")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
go m.watchPeerDisconnects()
m.stateMutex.Lock()
m.state.Screensaver.Available = true
m.state.Screensaver.Active = false
m.state.Screensaver.Inhibited = false
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
m.stateMutex.Unlock()
log.Info("Screensaver inhibit listener initialized")
log.Info("Screensaver listener initialized")
return nil
}
// exportScreensaverOnPaths exports the handler and introspection on the given
// paths under the specified interface name.
func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error {
iface := screensaverIntrospectIface(ifaceName)
for _, path := range paths {
if err := m.sessionConn.Export(handler, path, ifaceName); err != nil {
return fmt.Errorf("export handler on %s: %w", path, err)
}
node := &introspect.Node{
Name: string(path),
Interfaces: []introspect.Interface{
introspect.IntrospectData,
iface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", path, err)
}
}
return nil
}
@@ -268,3 +303,53 @@ func (m *Manager) NotifyScreensaverSubscribers() {
return true
})
}
func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
h.manager.stateMutex.RLock()
active := h.manager.state.Screensaver.Active
h.manager.stateMutex.RUnlock()
return active, nil
}
func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
h.manager.SetScreenLockActive(active)
return nil
}
func (h *screensaverHandler) Lock() *dbus.Error {
h.manager.SetScreenLockActive(true)
return nil
}
// SetScreenLockActive updates the screensaver active (locked) state and emits
// ActiveChanged on all claimed session bus interfaces.
func (m *Manager) SetScreenLockActive(active bool) {
m.stateMutex.Lock()
changed := m.state.Screensaver.Active != active
m.state.Screensaver.Active = active
m.stateMutex.Unlock()
if !changed {
return
}
log.Infof("Screen lock active changed: %v", active)
if m.sessionConn != nil {
if m.screensaverFreedesktopClaimed {
if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
}
if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
}
}
if m.screensaverGnomeClaimed {
if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
}
}
}
m.NotifyScreensaverSubscribers()
}

View File

@@ -0,0 +1,102 @@
package freedesktop
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSetScreenLockActive_ChangesState(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true},
},
stateMutex: sync.RWMutex{},
}
assert.False(t, manager.GetScreensaverState().Active)
manager.SetScreenLockActive(true)
assert.True(t, manager.GetScreensaverState().Active)
manager.SetScreenLockActive(false)
assert.False(t, manager.GetScreensaverState().Active)
}
func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) {
ch := make(chan ScreensaverState, 64)
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: false},
},
stateMutex: sync.RWMutex{},
}
manager.screensaverSubscribers.Store("test", ch)
defer manager.screensaverSubscribers.Delete("test")
// Setting to same value should not notify
manager.SetScreenLockActive(false)
select {
case <-ch:
t.Fatal("should not have received notification for no-change")
case <-time.After(50 * time.Millisecond):
// Expected: no notification
}
}
func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) {
ch := make(chan ScreensaverState, 64)
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: false},
},
stateMutex: sync.RWMutex{},
}
manager.screensaverSubscribers.Store("test", ch)
defer manager.screensaverSubscribers.Delete("test")
manager.SetScreenLockActive(true)
select {
case state := <-ch:
assert.True(t, state.Active)
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber notification")
}
}
func TestSetScreenLockActive_NilSessionConn(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true},
},
stateMutex: sync.RWMutex{},
}
assert.NotPanics(t, func() {
manager.SetScreenLockActive(true)
})
assert.True(t, manager.GetScreensaverState().Active)
}
func TestGetActive_ReturnsCurrentState(t *testing.T) {
manager := &Manager{
state: &FreedeskState{
Screensaver: ScreensaverState{Available: true, Active: true},
},
stateMutex: sync.RWMutex{},
}
handler := &screensaverHandler{manager: manager}
active, dbusErr := handler.GetActive()
assert.Nil(t, dbusErr)
assert.True(t, active)
}
func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) {
state := ScreensaverState{}
assert.False(t, state.Active)
}

View File

@@ -39,6 +39,7 @@ type ScreensaverInhibitor struct {
type ScreensaverState struct {
Available bool `json:"available"`
Active bool `json:"active"`
Inhibited bool `json:"inhibited"`
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
}
@@ -50,14 +51,16 @@ type FreedeskState struct {
}
type Manager struct {
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
screensaverFreedesktopClaimed bool
screensaverGnomeClaimed bool
}

View File

@@ -1516,7 +1516,11 @@ func Start(printDocs bool) error {
}
}()
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil {
log.Warnf("Loginctl manager unavailable: %v", err)
} else {
@@ -1525,6 +1529,7 @@ func Start(printDocs bool) error {
}()
go func() {
defer close(freedesktopReady)
if err := InitializeFreedeskManager(); err != nil {
log.Warnf("Freedesktop manager unavailable: %v", err)
} else if freedesktopManager != nil {
@@ -1533,6 +1538,31 @@ func Start(printDocs bool) error {
}
}()
// Bridge loginctl lock state to the freedesktop/gnome screensaver
// ActiveChanged signal so apps like Bitwarden can detect screen lock.
go func() {
<-loginctlReady
<-freedesktopReady
if loginctlManager == nil || freedesktopManager == nil {
return
}
ch := loginctlManager.Subscribe("dms-lock-bridge")
defer loginctlManager.Unsubscribe("dms-lock-bridge")
initial := loginctlManager.GetState()
lastLocked := initial.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
for state := range ch {
if state.Locked != lastLocked {
lastLocked = state.Locked
freedesktopManager.SetScreenLockActive(lastLocked)
}
}
}()
if err := InitializeWaylandManager(); err != nil {
log.Warnf("Wayland manager unavailable: %v", err)
}