diff options
| author | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
|---|---|---|
| committer | Nippy <nippy@rp1.hu> | 2026-05-09 13:33:09 +0200 |
| commit | a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5 (patch) | |
| tree | 1a8769217f84bfe9d6216fafaadaf8cdd876d457 /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl | |
| parent | 34b9c34f982b2596cfa9e368a2d7f74e9a17477c (diff) | |
| download | RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.tar.gz RaveOS-PKGBUILD-a2b924d7e9492b978ad6dc33b6c1ffcb304b4bc5.zip | |
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl')
10 files changed, 0 insertions, 2352 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go deleted file mode 100644 index 858248e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/actions.go +++ /dev/null @@ -1,88 +0,0 @@ -package loginctl - -import ( - "fmt" -) - -func (m *Manager) Lock() error { - if m.sessionObj == nil { - return fmt.Errorf("session object not available") - } - err := m.sessionObj.Call(dbusSessionInterface+".Lock", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Lock", 0).Err - } - if err != nil { - return fmt.Errorf("failed to lock session: %w", err) - } - } - return nil -} - -func (m *Manager) Unlock() error { - err := m.sessionObj.Call(dbusSessionInterface+".Unlock", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Unlock", 0).Err - } - if err != nil { - return fmt.Errorf("failed to unlock session: %w", err) - } - } - return nil -} - -func (m *Manager) Activate() error { - err := m.sessionObj.Call(dbusSessionInterface+".Activate", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Activate", 0).Err - } - if err != nil { - return fmt.Errorf("failed to activate session: %w", err) - } - } - return nil -} - -func (m *Manager) SetIdleHint(idle bool) error { - err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err - } - if err != nil { - return fmt.Errorf("failed to set idle hint: %w", err) - } - } - return nil -} - -func (m *Manager) Terminate() error { - err := m.sessionObj.Call(dbusSessionInterface+".Terminate", 0).Err - if err != nil { - if refreshErr := m.refreshSessionBinding(); refreshErr == nil { - err = m.sessionObj.Call(dbusSessionInterface+".Terminate", 0).Err - } - if err != nil { - return fmt.Errorf("failed to terminate session: %w", err) - } - } - return nil -} - -func (m *Manager) SetLockBeforeSuspend(enabled bool) { - m.lockBeforeSuspend.Store(enabled) -} - -func (m *Manager) SetSleepInhibitorEnabled(enabled bool) { - m.sleepInhibitorEnabled.Store(enabled) - if enabled { - // Re-acquire inhibitor if enabled - m.acquireSleepInhibitor() - } else { - // Release inhibitor if disabled - m.releaseSleepInhibitor() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go deleted file mode 100644 index 1a29588..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package loginctl - -const ( - dbusDest = "org.freedesktop.login1" - dbusPath = "/org/freedesktop/login1" - dbusManagerInterface = "org.freedesktop.login1.Manager" - dbusSessionInterface = "org.freedesktop.login1.Session" - dbusUserInterface = "org.freedesktop.login1.User" - dbusPropsInterface = "org.freedesktop.DBus.Properties" -) diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go deleted file mode 100644 index 564e996..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers.go +++ /dev/null @@ -1,156 +0,0 @@ -package loginctl - -import ( - "encoding/json" - "fmt" - "net" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params" -) - -func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { - switch req.Method { - case "loginctl.getState": - handleGetState(conn, req, manager) - case "loginctl.lock": - handleLock(conn, req, manager) - case "loginctl.unlock": - handleUnlock(conn, req, manager) - case "loginctl.activate": - handleActivate(conn, req, manager) - case "loginctl.setIdleHint": - handleSetIdleHint(conn, req, manager) - case "loginctl.setLockBeforeSuspend": - handleSetLockBeforeSuspend(conn, req, manager) - case "loginctl.setSleepInhibitorEnabled": - handleSetSleepInhibitorEnabled(conn, req, manager) - case "loginctl.lockerReady": - handleLockerReady(conn, req, manager) - case "loginctl.terminate": - handleTerminate(conn, req, manager) - case "loginctl.subscribe": - handleSubscribe(conn, req, manager) - default: - models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) - } -} - -func handleGetState(conn net.Conn, req models.Request, manager *Manager) { - models.Respond(conn, req.ID, manager.GetState()) -} - -func handleLock(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Lock(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked"}) -} - -func handleUnlock(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Unlock(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "unlocked"}) -} - -func handleActivate(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Activate(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "activated"}) -} - -func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) { - idle, err := params.Bool(req.Params, "idle") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - if err := manager.SetIdleHint(idle); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"}) -} - -func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetLockBeforeSuspend(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lock before suspend set"}) -} - -func handleSetSleepInhibitorEnabled(conn net.Conn, req models.Request, manager *Manager) { - enabled, err := params.Bool(req.Params, "enabled") - if err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - - manager.SetSleepInhibitorEnabled(enabled) - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "sleep inhibitor setting updated"}) -} - -func handleLockerReady(conn net.Conn, req models.Request, manager *Manager) { - manager.lockTimerMu.Lock() - if manager.lockTimer != nil { - manager.lockTimer.Stop() - manager.lockTimer = nil - } - manager.lockTimerMu.Unlock() - - id := manager.sleepCycleID.Load() - manager.releaseForCycle(id) - - if manager.inSleepCycle.Load() { - manager.signalLockerReady() - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "ok"}) -} - -func handleTerminate(conn net.Conn, req models.Request, manager *Manager) { - if err := manager.Terminate(); err != nil { - models.RespondError(conn, req.ID, err.Error()) - return - } - models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "terminated"}) -} - -func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) { - clientID := fmt.Sprintf("client-%p", conn) - stateChan := manager.Subscribe(clientID) - defer manager.Unsubscribe(clientID) - - initialState := manager.GetState() - event := SessionEvent{ - Type: EventStateChanged, - Data: initialState, - } - if err := json.NewEncoder(conn).Encode(models.Response[SessionEvent]{ - ID: req.ID, - Result: &event, - }); err != nil { - return - } - - for state := range stateChan { - event := SessionEvent{ - Type: EventStateChanged, - Data: state, - } - if err := json.NewEncoder(conn).Encode(models.Response[SessionEvent]{ - Result: &event, - }); err != nil { - return - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go deleted file mode 100644 index d3169e4..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/handlers_test.go +++ /dev/null @@ -1,500 +0,0 @@ -package loginctl - -import ( - "bytes" - "encoding/json" - "net" - "sync" - "testing" - "time" - - mockdbus "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/godbus/dbus/v5" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -type mockNetConn struct { - net.Conn - readBuf *bytes.Buffer - writeBuf *bytes.Buffer - closed bool -} - -func newMockNetConn() *mockNetConn { - return &mockNetConn{ - readBuf: &bytes.Buffer{}, - writeBuf: &bytes.Buffer{}, - } -} - -func (m *mockNetConn) Read(b []byte) (n int, err error) { - return m.readBuf.Read(b) -} - -func (m *mockNetConn) Write(b []byte) (n int, err error) { - return m.writeBuf.Write(b) -} - -func (m *mockNetConn) Close() error { - m.closed = true - return nil -} - -func TestRespondError_Loginctl(t *testing.T) { - conn := newMockNetConn() - models.RespondError(conn, 123, "test error") - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Equal(t, "test error", resp.Error) - assert.Nil(t, resp.Result) -} - -func TestRespond_Loginctl(t *testing.T) { - conn := newMockNetConn() - result := models.SuccessResult{Success: true, Message: "test"} - models.Respond(conn, 123, result) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "test", resp.Result.Message) -} - -func TestHandleGetState(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - SessionType: "wayland", - SessionClass: "user", - UserName: "testuser", - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.getState"} - - handleGetState(conn, req, manager) - - var resp models.Response[SessionState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.Equal(t, "1", resp.Result.SessionID) - assert.False(t, resp.Result.Locked) - assert.True(t, resp.Result.Active) -} - -func TestHandleLock(t *testing.T) { - t.Run("successful lock", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.lock"} - handleLock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "locked", resp.Result.Message) - }) - - t.Run("lock fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.lock"} - handleLock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to lock session") - }) -} - -func TestHandleUnlock(t *testing.T) { - t.Run("successful unlock", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Unlock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.unlock"} - handleUnlock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "unlocked", resp.Result.Message) - }) - - t.Run("unlock fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Unlock", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.unlock"} - handleUnlock(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to unlock session") - }) -} - -func TestHandleActivate(t *testing.T) { - t.Run("successful activate", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Activate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.activate"} - handleActivate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "activated", resp.Result.Message) - }) - - t.Run("activate fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Activate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.activate"} - handleActivate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to activate session") - }) -} - -func TestHandleSetIdleHint(t *testing.T) { - t.Run("missing idle parameter", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{}, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "missing or invalid 'idle' parameter") - }) - - t.Run("successful set idle hint true", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.SetIdleHint", dbus.Flags(0), true).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{ - "idle": true, - }, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "idle hint set", resp.Result.Message) - }) - - t.Run("set idle hint fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.SetIdleHint", dbus.Flags(0), false).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.setIdleHint", - Params: map[string]any{ - "idle": false, - }, - } - - handleSetIdleHint(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to set idle hint") - }) -} - -func TestHandleTerminate(t *testing.T) { - t.Run("successful terminate", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Terminate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.terminate"} - handleTerminate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - require.NotNil(t, resp.Result) - assert.True(t, resp.Result.Success) - assert.Equal(t, "terminated", resp.Result.Message) - }) - - t.Run("terminate fails", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: assert.AnError} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Terminate", dbus.Flags(0)).Return(mockCall) - - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - sessionObj: mockSessionObj, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.terminate"} - handleTerminate(conn, req, manager) - - var resp models.Response[models.SuccessResult] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "failed to terminate session") - }) -} - -func TestHandleRequest(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - } - - t.Run("unknown method", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.unknown", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Contains(t, resp.Error, "unknown method") - }) - - t.Run("valid method - getState", func(t *testing.T) { - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.getState", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[SessionState] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - assert.Empty(t, resp.Error) - }) - - t.Run("lock method", func(t *testing.T) { - mockSessionObj := mockdbus.NewMockBusObject(t) - mockCall := &dbus.Call{Err: nil} - mockSessionObj.EXPECT().Call("org.freedesktop.login1.Session.Lock", mock.Anything).Return(mockCall) - - manager.sessionObj = mockSessionObj - - conn := newMockNetConn() - req := models.Request{ - ID: 123, - Method: "loginctl.lock", - } - - HandleRequest(conn, req, manager) - - var resp models.Response[any] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - require.NoError(t, err) - - assert.Equal(t, 123, resp.ID) - }) -} - -func TestHandleSubscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - } - - conn := newMockNetConn() - req := models.Request{ID: 123, Method: "loginctl.subscribe"} - - done := make(chan bool) - go func() { - handleSubscribe(conn, req, manager) - done <- true - }() - - time.Sleep(50 * time.Millisecond) - - conn.Close() - - if conn.writeBuf.Len() > 0 { - var resp models.Response[SessionEvent] - err := json.NewDecoder(conn.writeBuf).Decode(&resp) - if err == nil { - assert.Equal(t, 123, resp.ID) - require.NotNil(t, resp.Result) - assert.Equal(t, EventStateChanged, resp.Result.Type) - assert.Equal(t, "1", resp.Result.Data.SessionID) - } - } - - select { - case <-done: - case <-time.After(100 * time.Millisecond): - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go deleted file mode 100644 index 0c51e64..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager.go +++ /dev/null @@ -1,723 +0,0 @@ -package loginctl - -import ( - "context" - "fmt" - "os" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func NewManager() (*Manager, error) { - conn, err := dbus.ConnectSystemBus() - if err != nil { - return nil, fmt.Errorf("failed to connect to system bus: %w", err) - } - - m := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - - stopChan: make(chan struct{}), - conn: conn, - dirty: make(chan struct{}, 1), - signals: make(chan *dbus.Signal, 256), - } - m.sleepInhibitorEnabled.Store(true) - - if err := m.initialize(); err != nil { - conn.Close() - return nil, err - } - - if err := m.acquireSleepInhibitor(); err != nil { - fmt.Fprintf(os.Stderr, "sleep inhibitor unavailable: %v\n", err) - } - - m.notifierWg.Add(1) - go m.notifier() - - if err := m.startSignalPump(); err != nil { - m.Close() - return nil, err - } - - return m, nil -} - -func (m *Manager) initialize() error { - m.managerObj = m.conn.Object(dbusDest, dbus.ObjectPath(dbusPath)) - - m.initializeFallbackDelay() - - sessionID, sessionPath, err := m.discoverSession() - if err != nil { - return fmt.Errorf("failed to get session path: %w", err) - } - - m.stateMutex.Lock() - m.state.SessionID = sessionID - m.state.SessionPath = string(sessionPath) - m.sessionPath = sessionPath - m.stateMutex.Unlock() - - m.sessionObj = m.conn.Object(dbusDest, sessionPath) - - if err := m.updateSessionState(); err != nil { - return err - } - - return nil -} - -func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) { - // 1. Explicit XDG_SESSION_ID - if id := os.Getenv("XDG_SESSION_ID"); id != "" { - if path, err := m.getSession(id); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id) - return id, path, nil - } - } - - // 2. PID-based lookup (works when caller is inside a session cgroup) - if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id) - return id, path, nil - } - - // 3. User's primary display session (handles UWSM and similar) - if id, path, err := m.getUserDisplaySession(); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id) - return id, path, nil - } - - // 4. Score all sessions for current UID - if id, path, err := m.findBestSession(); err == nil { - fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id) - return id, path, nil - } - - // 5. Last resort: "self" - path, err := m.getSession("self") - if err != nil { - return "", "", fmt.Errorf("%w", err) - } - return "self", path, nil -} - -func (m *Manager) getSession(id string) (dbus.ObjectPath, error) { - var out dbus.ObjectPath - err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out) - if err != nil { - return "", err - } - return out, nil -} - -func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) { - var path dbus.ObjectPath - if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil { - return "", "", err - } - - sessionObj := m.conn.Object(dbusDest, path) - var id dbus.Variant - if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil { - return "", "", err - } - return id.Value().(string), path, nil -} - -func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) { - uid := uint32(os.Getuid()) - - var userPath dbus.ObjectPath - if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil { - return "", "", err - } - - userObj := m.conn.Object(dbusDest, userPath) - var display dbus.Variant - if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil { - return "", "", err - } - - pair, ok := display.Value().([]any) - if !ok || len(pair) < 2 { - return "", "", fmt.Errorf("unexpected Display format") - } - - sessionID, _ := pair[0].(string) - sessionPath, _ := pair[1].(dbus.ObjectPath) - if sessionID == "" || sessionPath == "" { - return "", "", fmt.Errorf("empty Display session") - } - - return sessionID, sessionPath, nil -} - -type sessionCandidate struct { - id string - path dbus.ObjectPath -} - -func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) { - // ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path] - var raw [][]any - if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil { - return "", "", err - } - - uid := uint32(os.Getuid()) - var candidates []sessionCandidate - for _, entry := range raw { - if len(entry) < 5 { - continue - } - entryUID, _ := entry[1].(uint32) - if entryUID != uid { - continue - } - id, _ := entry[0].(string) - path, _ := entry[4].(dbus.ObjectPath) - if id != "" && path != "" { - candidates = append(candidates, sessionCandidate{id: id, path: path}) - } - } - if len(candidates) == 0 { - return "", "", fmt.Errorf("no sessions for uid %d", uid) - } - - bestScore := -1 - var best sessionCandidate - for _, c := range candidates { - score := m.scoreSession(c.path) - if score > bestScore { - bestScore = score - best = c - } - } - if bestScore < 0 { - return "", "", fmt.Errorf("no viable session found") - } - return best.id, best.path, nil -} - -func (m *Manager) scoreSession(path dbus.ObjectPath) int { - obj := m.conn.Object(dbusDest, path) - var props map[string]dbus.Variant - if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil { - return -1 - } - - getStr := func(key string) string { - if v, ok := props[key]; ok { - if s, ok := v.Value().(string); ok { - return s - } - } - return "" - } - getBool := func(key string) bool { - if v, ok := props[key]; ok { - if b, ok := v.Value().(bool); ok { - return b - } - } - return false - } - getUint32 := func(key string) uint32 { - if v, ok := props[key]; ok { - if u, ok := v.Value().(uint32); ok { - return u - } - } - return 0 - } - - class := getStr("Class") - if class != "user" { - return -1 - } - if getBool("Remote") { - return -1 - } - - score := 0 - - if getBool("Active") { - score += 100 - } - - switch getStr("Type") { - case "wayland", "x11": - score += 80 - case "tty": - score += 10 - } - - if v, ok := props["Seat"]; ok { - if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 { - if seat, ok := seatArr[0].(string); ok && seat != "" { - score += 40 - if seat == "seat0" { - score += 10 - } - } - } - } - - if getUint32("VTNr") > 0 { - score += 20 - } - - return score -} - -func (m *Manager) refreshSessionBinding() error { - if m.managerObj == nil || m.conn == nil { - return fmt.Errorf("manager not fully initialized") - } - - sessionPath, err := m.getSession(m.state.SessionID) - if err != nil { - return fmt.Errorf("failed to get session path: %w", err) - } - - m.stateMutex.RLock() - currentPath := m.sessionPath - m.stateMutex.RUnlock() - - if sessionPath == currentPath { - return nil - } - - m.stopSignalPump() - - m.stateMutex.Lock() - m.state.SessionPath = string(sessionPath) - m.sessionPath = sessionPath - m.stateMutex.Unlock() - - m.sessionObj = m.conn.Object(dbusDest, sessionPath) - - if err := m.updateSessionState(); err != nil { - return err - } - - m.signals = make(chan *dbus.Signal, 256) - return m.startSignalPump() -} - -func (m *Manager) updateSessionState() error { - ctx := context.Background() - props, err := m.getSessionProperties(ctx) - if err != nil { - return err - } - - m.stateMutex.Lock() - defer m.stateMutex.Unlock() - - m.state.Active = dbusutil.GetOr(props, "Active", m.state.Active) - m.state.IdleHint = dbusutil.GetOr(props, "IdleHint", m.state.IdleHint) - m.state.IdleSinceHint = dbusutil.GetOr(props, "IdleSinceHint", m.state.IdleSinceHint) - if lockedHint, ok := dbusutil.Get[bool](props, "LockedHint"); ok { - m.state.LockedHint = lockedHint - m.state.Locked = lockedHint - } - m.state.SessionType = dbusutil.GetOr(props, "Type", m.state.SessionType) - m.state.SessionClass = dbusutil.GetOr(props, "Class", m.state.SessionClass) - if v, ok := props["User"]; ok { - if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 { - if uid, ok := userArr[0].(uint32); ok { - m.state.User = uid - } - } - } - m.state.UserName = dbusutil.GetOr(props, "Name", m.state.UserName) - m.state.RemoteHost = dbusutil.GetOr(props, "RemoteHost", m.state.RemoteHost) - m.state.Service = dbusutil.GetOr(props, "Service", m.state.Service) - m.state.TTY = dbusutil.GetOr(props, "TTY", m.state.TTY) - m.state.Display = dbusutil.GetOr(props, "Display", m.state.Display) - m.state.Remote = dbusutil.GetOr(props, "Remote", m.state.Remote) - if v, ok := props["Seat"]; ok { - if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 { - if seatID, ok := seatArr[0].(string); ok { - m.state.Seat = seatID - } - } - } - m.state.VTNr = dbusutil.GetOr(props, "VTNr", m.state.VTNr) - - return nil -} - -func (m *Manager) getSessionProperties(ctx context.Context) (map[string]dbus.Variant, error) { - var props map[string]dbus.Variant - err := m.sessionObj.CallWithContext(ctx, dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props) - if err != nil { - return nil, err - } - return props, nil -} - -func (m *Manager) acquireSleepInhibitor() error { - if !m.sleepInhibitorEnabled.Load() { - return nil - } - - m.inhibitMu.Lock() - defer m.inhibitMu.Unlock() - - if m.inhibitFile != nil { - return nil - } - - if m.managerObj == nil { - return fmt.Errorf("manager object not available") - } - - file, err := m.inhibit("sleep", "DankMaterialShell", "Lock before suspend", "delay") - if err != nil { - return err - } - - m.inhibitFile = file - return nil -} - -func (m *Manager) inhibit(what, who, why, mode string) (*os.File, error) { - var fd dbus.UnixFD - err := m.managerObj.Call(dbusManagerInterface+".Inhibit", 0, what, who, why, mode).Store(&fd) - if err != nil { - return nil, err - } - return os.NewFile(uintptr(fd), "inhibit"), nil -} - -func (m *Manager) releaseSleepInhibitor() { - m.inhibitMu.Lock() - f := m.inhibitFile - m.inhibitFile = nil - m.inhibitMu.Unlock() - if f != nil { - f.Close() - } -} - -func (m *Manager) releaseForCycle(id uint64) { - if !m.inSleepCycle.Load() || m.sleepCycleID.Load() != id { - return - } - m.releaseSleepInhibitor() -} - -func (m *Manager) initializeFallbackDelay() { - var maxDelayUSec uint64 - err := m.managerObj.Call( - dbusPropsInterface+".Get", - 0, - dbusManagerInterface, - "InhibitDelayMaxUSec", - ).Store(&maxDelayUSec) - - if err != nil { - m.fallbackDelay = 2 * time.Second - return - } - - maxDelay := time.Duration(maxDelayUSec) * time.Microsecond - computed := (maxDelay * 8) / 10 - - if computed < 2*time.Second { - m.fallbackDelay = 2 * time.Second - } else if computed > 4*time.Second { - m.fallbackDelay = 4 * time.Second - } else { - m.fallbackDelay = computed - } -} - -func (m *Manager) newLockerReadyCh() chan struct{} { - m.lockerReadyChMu.Lock() - defer m.lockerReadyChMu.Unlock() - m.lockerReadyCh = make(chan struct{}) - return m.lockerReadyCh -} - -func (m *Manager) signalLockerReady() { - m.lockerReadyChMu.Lock() - ch := m.lockerReadyCh - if ch != nil { - close(ch) - m.lockerReadyCh = nil - } - m.lockerReadyChMu.Unlock() -} - -func (m *Manager) snapshotState() SessionState { - m.stateMutex.RLock() - defer m.stateMutex.RUnlock() - return *m.state -} - -func stateChangedMeaningfully(old, new *SessionState) bool { - if old.Locked != new.Locked { - return true - } - if old.LockedHint != new.LockedHint { - return true - } - if old.Active != new.Active { - return true - } - if old.IdleHint != new.IdleHint { - return true - } - if old.PreparingForSleep != new.PreparingForSleep { - return true - } - return false -} - -func (m *Manager) GetState() SessionState { - return m.snapshotState() -} - -func (m *Manager) Subscribe(id string) chan SessionState { - ch := make(chan SessionState, 64) - m.subscribers.Store(id, ch) - return ch -} - -func (m *Manager) Unsubscribe(id string) { - if val, ok := m.subscribers.LoadAndDelete(id); ok { - close(val) - } -} - -func (m *Manager) notifier() { - defer m.notifierWg.Done() - const minGap = 100 * time.Millisecond - timer := time.NewTimer(minGap) - timer.Stop() - var pending bool - for { - select { - case <-m.stopChan: - timer.Stop() - return - case <-m.dirty: - if pending { - continue - } - pending = true - timer.Reset(minGap) - case <-timer.C: - if !pending { - continue - } - - currentState := m.snapshotState() - - if m.lastNotifiedState != nil && !stateChangedMeaningfully(m.lastNotifiedState, ¤tState) { - pending = false - continue - } - - m.subscribers.Range(func(key string, ch chan SessionState) bool { - select { - case ch <- currentState: - default: - } - return true - }) - - stateCopy := currentState - m.lastNotifiedState = &stateCopy - pending = false - } - } -} - -func (m *Manager) notifySubscribers() { - select { - case m.dirty <- struct{}{}: - default: - } -} - -func (m *Manager) startSignalPump() error { - m.conn.Signal(m.signals) - - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ); err != nil { - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - - if err := m.conn.AddMatchSignal( - dbus.WithMatchObjectPath("/org/freedesktop/DBus"), - dbus.WithMatchInterface("org.freedesktop.DBus"), - dbus.WithMatchMember("NameOwnerChanged"), - ); err != nil { - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ) - m.conn.RemoveSignal(m.signals) - return err - } - - m.sigWG.Add(1) - go func() { - defer m.sigWG.Done() - for { - select { - case <-m.stopChan: - return - case sig, ok := <-m.signals: - if !ok { - return - } - if sig == nil { - continue - } - m.handleDBusSignal(sig) - } - } - }() - return nil -} - -func (m *Manager) stopSignalPump() { - if m.conn == nil { - return - } - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusPropsInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Lock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(m.sessionPath), - dbus.WithMatchInterface(dbusSessionInterface), - dbus.WithMatchMember("Unlock"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath(dbus.ObjectPath(dbusPath)), - dbus.WithMatchInterface(dbusManagerInterface), - dbus.WithMatchMember("PrepareForSleep"), - ) - m.conn.RemoveMatchSignal( - dbus.WithMatchObjectPath("/org/freedesktop/DBus"), - dbus.WithMatchInterface("org.freedesktop.DBus"), - dbus.WithMatchMember("NameOwnerChanged"), - ) - - m.conn.RemoveSignal(m.signals) - close(m.signals) - - m.sigWG.Wait() -} - -func (m *Manager) Close() { - close(m.stopChan) - m.notifierWg.Wait() - - m.stopSignalPump() - - m.releaseSleepInhibitor() - - m.subscribers.Range(func(key string, ch chan SessionState) bool { - close(ch) - m.subscribers.Delete(key) - return true - }) - - if m.conn != nil { - m.conn.Close() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go deleted file mode 100644 index 148ee60..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/manager_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package loginctl - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestManager_GetState(t *testing.T) { - state := &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - IdleHint: false, - SessionType: "wayland", - SessionClass: "user", - UserName: "testuser", - } - - manager := &Manager{ - state: state, - stateMutex: sync.RWMutex{}, - } - - result := manager.GetState() - assert.Equal(t, "1", result.SessionID) - assert.False(t, result.Locked) - assert.True(t, result.Active) - assert.Equal(t, "wayland", result.SessionType) - assert.Equal(t, "testuser", result.UserName) -} - -func TestManager_Subscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - ch := manager.Subscribe("test-client") - assert.NotNil(t, ch) - assert.Equal(t, 64, cap(ch)) - - _, exists := manager.subscribers.Load("test-client") - assert.True(t, exists) -} - -func TestManager_Unsubscribe(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - ch := manager.Subscribe("test-client") - - manager.Unsubscribe("test-client") - - _, ok := <-ch - assert.False(t, ok) - - _, exists := manager.subscribers.Load("test-client") - assert.False(t, exists) -} - -func TestManager_Unsubscribe_NonExistent(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - } - - // Unsubscribe a non-existent client should not panic - assert.NotPanics(t, func() { - manager.Unsubscribe("non-existent") - }) -} - -func TestManager_NotifySubscribers(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan SessionState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - - select { - case state := <-ch: - assert.Equal(t, "1", state.SessionID) - assert.False(t, state.Locked) - case <-time.After(200 * time.Millisecond): - t.Fatal("did not receive state update") - } - - close(manager.stopChan) - manager.notifierWg.Wait() -} - -func TestManager_NotifySubscribers_Debounce(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - }, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - dirty: make(chan struct{}, 1), - } - manager.notifierWg.Add(1) - go manager.notifier() - - ch := make(chan SessionState, 10) - manager.subscribers.Store("test-client", ch) - - manager.notifySubscribers() - manager.notifySubscribers() - manager.notifySubscribers() - - receivedCount := 0 - timeout := time.After(200 * time.Millisecond) - for { - select { - case <-ch: - receivedCount++ - case <-timeout: - assert.Equal(t, 1, receivedCount, "should receive exactly one debounced update") - close(manager.stopChan) - manager.notifierWg.Wait() - return - } - } -} - -func TestManager_Close(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - stopChan: make(chan struct{}), - } - - ch1 := make(chan SessionState, 1) - ch2 := make(chan SessionState, 1) - manager.subscribers.Store("client1", ch1) - manager.subscribers.Store("client2", ch2) - - manager.Close() - - select { - case <-manager.stopChan: - case <-time.After(100 * time.Millisecond): - t.Fatal("stopChan not closed") - } - - _, ok1 := <-ch1 - _, ok2 := <-ch2 - assert.False(t, ok1, "ch1 should be closed") - assert.False(t, ok2, "ch2 should be closed") - - count := 0 - manager.subscribers.Range(func(key string, ch chan SessionState) bool { - count++ - return true - }) - assert.Equal(t, 0, count) -} - -func TestManager_GetState_ThreadSafe(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - }, - stateMutex: sync.RWMutex{}, - } - - done := make(chan bool) - for i := 0; i < 10; i++ { - go func() { - state := manager.GetState() - assert.Equal(t, "1", state.SessionID) - assert.True(t, state.Active) - done <- true - }() - } - - for i := 0; i < 10; i++ { - select { - case <-done: - case <-time.After(1 * time.Second): - t.Fatal("timeout waiting for goroutines") - } - } -} - -func TestStateChangedMeaningfully(t *testing.T) { - tests := []struct { - name string - old *SessionState - new *SessionState - expected bool - }{ - { - name: "no change", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: true, IdleHint: false}, - expected: false, - }, - { - name: "locked changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: true, Active: true, IdleHint: false}, - expected: true, - }, - { - name: "active changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: false, IdleHint: false}, - expected: true, - }, - { - name: "idle hint changed", - old: &SessionState{Locked: false, Active: true, IdleHint: false}, - new: &SessionState{Locked: false, Active: true, IdleHint: true}, - expected: true, - }, - { - name: "locked hint changed", - old: &SessionState{Locked: false, Active: true, LockedHint: false}, - new: &SessionState{Locked: false, Active: true, LockedHint: true}, - expected: true, - }, - { - name: "preparing for sleep changed", - old: &SessionState{Locked: false, Active: true, PreparingForSleep: false}, - new: &SessionState{Locked: false, Active: true, PreparingForSleep: true}, - expected: true, - }, - { - name: "non-meaningful change (username)", - old: &SessionState{Locked: false, Active: true, UserName: "user1"}, - new: &SessionState{Locked: false, Active: true, UserName: "user2"}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := stateChangedMeaningfully(tt.old, tt.new) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestManager_SnapshotState(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - SessionID: "1", - Locked: false, - Active: true, - UserName: "testuser", - }, - stateMutex: sync.RWMutex{}, - } - - snapshot := manager.snapshotState() - assert.Equal(t, "1", snapshot.SessionID) - assert.False(t, snapshot.Locked) - assert.True(t, snapshot.Active) - assert.Equal(t, "testuser", snapshot.UserName) - - snapshot.Locked = true - assert.False(t, manager.state.Locked) -} - -func TestNewManager(t *testing.T) { - t.Run("attempts to create manager", func(t *testing.T) { - manager, err := NewManager() - if err != nil { - assert.Nil(t, manager) - } else { - assert.NotNil(t, manager) - assert.NotNil(t, manager.state) - assert.NotNil(t, &manager.subscribers) - assert.NotNil(t, manager.stopChan) - - manager.Close() - } - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go deleted file mode 100644 index 4c0605a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor.go +++ /dev/null @@ -1,155 +0,0 @@ -package loginctl - -import ( - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil" - "github.com/godbus/dbus/v5" -) - -func (m *Manager) handleDBusSignal(sig *dbus.Signal) { - switch sig.Name { - case dbusSessionInterface + ".Lock": - m.stateMutex.Lock() - m.state.Locked = true - m.state.LockedHint = true - m.stateMutex.Unlock() - m.notifySubscribers() - - if m.sleepInhibitorEnabled.Load() && m.inSleepCycle.Load() { - id := m.sleepCycleID.Load() - m.lockTimerMu.Lock() - if m.lockTimer != nil { - m.lockTimer.Stop() - } - m.lockTimer = time.AfterFunc(m.fallbackDelay, func() { - m.releaseForCycle(id) - }) - m.lockTimerMu.Unlock() - } - - case dbusSessionInterface + ".Unlock": - m.stateMutex.Lock() - m.state.Locked = false - m.state.LockedHint = false - m.stateMutex.Unlock() - m.notifySubscribers() - - // Cancel the lock timer if it's still running - m.lockTimerMu.Lock() - if m.lockTimer != nil { - m.lockTimer.Stop() - m.lockTimer = nil - } - m.lockTimerMu.Unlock() - - // Re-acquire the sleep inhibitor (acquireSleepInhibitor checks the enabled flag) - m.acquireSleepInhibitor() - - case dbusManagerInterface + ".PrepareForSleep": - if len(sig.Body) == 0 { - return - } - preparing, _ := sig.Body[0].(bool) - - if preparing { - cycleID := m.sleepCycleID.Add(1) - m.inSleepCycle.Store(true) - - if m.lockBeforeSuspend.Load() { - m.Lock() - } - - readyCh := m.newLockerReadyCh() - go func(id uint64, ch <-chan struct{}) { - <-ch - if m.inSleepCycle.Load() && m.sleepCycleID.Load() == id { - m.releaseSleepInhibitor() - } - }(cycleID, readyCh) - } else { - m.inSleepCycle.Store(false) - m.signalLockerReady() - m.refreshSessionBinding() - m.acquireSleepInhibitor() - } - - m.stateMutex.Lock() - m.state.PreparingForSleep = preparing - m.stateMutex.Unlock() - m.notifySubscribers() - - case dbusPropsInterface + ".PropertiesChanged": - m.handlePropertiesChanged(sig) - - case "org.freedesktop.DBus.NameOwnerChanged": - if len(sig.Body) == 3 { - name, _ := sig.Body[0].(string) - oldOwner, _ := sig.Body[1].(string) - newOwner, _ := sig.Body[2].(string) - if name == dbusDest && oldOwner != "" && newOwner != "" { - m.updateSessionState() - if !m.inSleepCycle.Load() { - m.acquireSleepInhibitor() - } - m.notifySubscribers() - } - } - } -} - -func (m *Manager) handlePropertiesChanged(sig *dbus.Signal) { - if len(sig.Body) < 2 { - return - } - - iface, ok := sig.Body[0].(string) - if !ok || iface != dbusSessionInterface { - return - } - - changes, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - var needsUpdate bool - - for key, variant := range changes { - switch key { - case "Active": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.Active = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "IdleHint": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.IdleHint = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "IdleSinceHint": - if val, ok := dbusutil.As[uint64](variant); ok { - m.stateMutex.Lock() - m.state.IdleSinceHint = val - m.stateMutex.Unlock() - needsUpdate = true - } - case "LockedHint": - if val, ok := dbusutil.As[bool](variant); ok { - m.stateMutex.Lock() - m.state.LockedHint = val - m.state.Locked = val - m.stateMutex.Unlock() - needsUpdate = true - } - } - } - - if needsUpdate { - m.notifySubscribers() - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go deleted file mode 100644 index ecbb57f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/monitor_test.go +++ /dev/null @@ -1,298 +0,0 @@ -package loginctl - -import ( - "sync" - "testing" - - "github.com/godbus/dbus/v5" - "github.com/stretchr/testify/assert" -) - -func TestManager_HandleDBusSignal_Lock(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Locked: false, - LockedHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Session.Lock", - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Locked) - assert.True(t, manager.state.LockedHint) -} - -func TestManager_HandleDBusSignal_Unlock(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Locked: true, - LockedHint: true, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Session.Unlock", - } - - 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) { - t.Run("preparing for sleep - true", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{true}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.PreparingForSleep) - }) - - t.Run("preparing for sleep - false", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: true, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{false}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.PreparingForSleep) - }) - - t.Run("empty body", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - PreparingForSleep: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.login1.Manager.PrepareForSleep", - Body: []any{}, - } - - manager.handleDBusSignal(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.PreparingForSleep) - }) -} - -func TestManager_HandlePropertiesChanged(t *testing.T) { - t.Run("active property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Active) - }) - - t.Run("idle hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - IdleHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "IdleHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.IdleHint) - }) - - t.Run("idle since hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - IdleSinceHint: 0, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "IdleSinceHint": dbus.MakeVariant(uint64(123456789)), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.Equal(t, uint64(123456789), manager.state.IdleSinceHint) - }) - - t.Run("locked hint property changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - LockedHint: false, - Locked: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "LockedHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.LockedHint) - assert.True(t, manager.state.Locked) - }) - - t.Run("wrong interface", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.SomeOtherInterface", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.False(t, manager.state.Active) - }) - - t.Run("empty body", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{}, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{}, - } - - assert.NotPanics(t, func() { - manager.handlePropertiesChanged(sig) - }) - }) - - t.Run("multiple properties changed", func(t *testing.T) { - manager := &Manager{ - state: &SessionState{ - Active: false, - IdleHint: false, - }, - stateMutex: sync.RWMutex{}, - dirty: make(chan struct{}, 1), - } - - sig := &dbus.Signal{ - Name: "org.freedesktop.DBus.Properties.PropertiesChanged", - Body: []any{ - "org.freedesktop.login1.Session", - map[string]dbus.Variant{ - "Active": dbus.MakeVariant(true), - "IdleHint": dbus.MakeVariant(true), - }, - }, - } - - manager.handlePropertiesChanged(sig) - - manager.stateMutex.RLock() - defer manager.stateMutex.RUnlock() - assert.True(t, manager.state.Active) - assert.True(t, manager.state.IdleHint) - }) -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go deleted file mode 100644 index 76c7414..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types.go +++ /dev/null @@ -1,76 +0,0 @@ -package loginctl - -import ( - "os" - "sync" - "sync/atomic" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" - "github.com/godbus/dbus/v5" -) - -type SessionState struct { - SessionID string `json:"sessionId"` - SessionPath string `json:"sessionPath"` - Locked bool `json:"locked"` - Active bool `json:"active"` - IdleHint bool `json:"idleHint"` - IdleSinceHint uint64 `json:"idleSinceHint"` - LockedHint bool `json:"lockedHint"` - SessionType string `json:"sessionType"` - SessionClass string `json:"sessionClass"` - User uint32 `json:"user"` - UserName string `json:"userName"` - RemoteHost string `json:"remoteHost"` - Service string `json:"service"` - TTY string `json:"tty"` - Display string `json:"display"` - Remote bool `json:"remote"` - Seat string `json:"seat"` - VTNr uint32 `json:"vtnr"` - PreparingForSleep bool `json:"preparingForSleep"` -} - -type EventType string - -const ( - EventStateChanged EventType = "state_changed" - EventLock EventType = "lock" - EventUnlock EventType = "unlock" - EventPrepareForSleep EventType = "prepare_for_sleep" - EventIdleHintChanged EventType = "idle_hint_changed" - EventLockedHintChanged EventType = "locked_hint_changed" -) - -type SessionEvent struct { - Type EventType `json:"type"` - Data SessionState `json:"data"` -} - -type Manager struct { - state *SessionState - stateMutex sync.RWMutex - subscribers syncmap.Map[string, chan SessionState] - stopChan chan struct{} - conn *dbus.Conn - sessionPath dbus.ObjectPath - managerObj dbus.BusObject - sessionObj dbus.BusObject - dirty chan struct{} - notifierWg sync.WaitGroup - lastNotifiedState *SessionState - signals chan *dbus.Signal - sigWG sync.WaitGroup - inhibitMu sync.Mutex - inhibitFile *os.File - lockBeforeSuspend atomic.Bool - inSleepCycle atomic.Bool - sleepCycleID atomic.Uint64 - lockerReadyChMu sync.Mutex - lockerReadyCh chan struct{} - lockTimerMu sync.Mutex - lockTimer *time.Timer - sleepInhibitorEnabled atomic.Bool - fallbackDelay time.Duration -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go deleted file mode 100644 index 383300f..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/loginctl/types_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package loginctl - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEventType_Constants(t *testing.T) { - assert.Equal(t, EventType("state_changed"), EventStateChanged) - assert.Equal(t, EventType("lock"), EventLock) - assert.Equal(t, EventType("unlock"), EventUnlock) - assert.Equal(t, EventType("prepare_for_sleep"), EventPrepareForSleep) - assert.Equal(t, EventType("idle_hint_changed"), EventIdleHintChanged) - assert.Equal(t, EventType("locked_hint_changed"), EventLockedHintChanged) -} - -func TestSessionState_Struct(t *testing.T) { - state := SessionState{ - SessionID: "1", - Locked: false, - Active: true, - SessionType: "wayland", - User: 1000, - UserName: "testuser", - } - - assert.Equal(t, "1", state.SessionID) - assert.True(t, state.Active) - assert.False(t, state.Locked) - assert.Equal(t, "wayland", state.SessionType) - assert.Equal(t, uint32(1000), state.User) - assert.Equal(t, "testuser", state.UserName) -} - -func TestSessionEvent_Struct(t *testing.T) { - state := SessionState{ - SessionID: "1", - Locked: true, - } - - event := SessionEvent{ - Type: EventLock, - Data: state, - } - - assert.Equal(t, EventLock, event.Type) - assert.Equal(t, "1", event.Data.SessionID) - assert.True(t, event.Data.Locked) -} |