package freedesktop import ( "path/filepath" "strings" "sync/atomic" "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5/introspect" ) 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 } introNode := &introspect.Node{ Name: dbusScreensaverPath, Interfaces: []introspect.Interface{ introspect.IntrospectData, {Name: dbusScreensaverInterface}, }, } 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) } introNode2 := &introspect.Node{ Name: dbusScreensaverPath2, Interfaces: []introspect.Interface{ introspect.IntrospectData, {Name: dbusScreensaverInterface}, }, } 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) } go m.watchPeerDisconnects() m.stateMutex.Lock() m.state.Screensaver.Available = true m.state.Screensaver.Inhibited = false m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{} m.stateMutex.Unlock() log.Info("Screensaver inhibit listener initialized") return nil } func (h *screensaverHandler) Inhibit(sender dbus.Sender, appName, reason string) (uint32, *dbus.Error) { if appName == "" { return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"application name required"}) } if reason == "" { return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"reason required"}) } if strings.Contains(strings.ToLower(reason), "audio") && !strings.Contains(strings.ToLower(reason), "video") { log.Debugf("Ignoring audio-only inhibit from %s: %s", appName, reason) return 0, nil } if idx := strings.LastIndex(appName, "/"); idx != -1 && idx < len(appName)-1 { appName = appName[idx+1:] } appName = filepath.Base(appName) cookie := atomic.AddUint32(&h.manager.screensaverCookieCounter, 1) inhibitor := ScreensaverInhibitor{ Cookie: cookie, AppName: appName, Reason: reason, Peer: string(sender), StartTime: time.Now().Unix(), } h.manager.stateMutex.Lock() h.manager.state.Screensaver.Inhibitors = append(h.manager.state.Screensaver.Inhibitors, inhibitor) h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0 h.manager.stateMutex.Unlock() log.Infof("Screensaver inhibited by %s (%s): %s -> cookie %08X", appName, sender, reason, cookie) h.manager.NotifyScreensaverSubscribers() return cookie, nil } func (h *screensaverHandler) UnInhibit(sender dbus.Sender, cookie uint32) *dbus.Error { h.manager.stateMutex.Lock() defer h.manager.stateMutex.Unlock() found := false inhibitors := h.manager.state.Screensaver.Inhibitors for i, inh := range inhibitors { if inh.Cookie != cookie { continue } log.Infof("Screensaver uninhibited by %s (%s) cookie %08X", inh.AppName, sender, cookie) h.manager.state.Screensaver.Inhibitors = append(inhibitors[:i], inhibitors[i+1:]...) found = true break } if !found { log.Debugf("UnInhibit: no match for cookie %08X", cookie) return nil } h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0 go h.manager.NotifyScreensaverSubscribers() return nil } func (m *Manager) watchPeerDisconnects() { if m.sessionConn == nil { return } if err := m.sessionConn.AddMatchSignal( dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged"), ); err != nil { log.Warnf("Failed to watch peer disconnects: %v", err) return } signals := make(chan *dbus.Signal, 64) m.sessionConn.Signal(signals) for sig := range signals { if sig.Name != "org.freedesktop.DBus.NameOwnerChanged" { continue } if len(sig.Body) < 3 { continue } name, ok1 := sig.Body[0].(string) newOwner, ok2 := sig.Body[2].(string) if !ok1 || !ok2 { continue } if newOwner != "" { continue } m.removeInhibitorsByPeer(name) } } func (m *Manager) removeInhibitorsByPeer(peer string) { m.stateMutex.Lock() defer m.stateMutex.Unlock() var remaining []ScreensaverInhibitor var removed []ScreensaverInhibitor for _, inh := range m.state.Screensaver.Inhibitors { if inh.Peer == peer { removed = append(removed, inh) continue } remaining = append(remaining, inh) } if len(removed) == 0 { return } for _, inh := range removed { log.Infof("Screensaver: peer %s died, removing inhibitor from %s (cookie %08X)", peer, inh.AppName, inh.Cookie) } m.state.Screensaver.Inhibitors = remaining m.state.Screensaver.Inhibited = len(remaining) > 0 go m.NotifyScreensaverSubscribers() } func (m *Manager) GetScreensaverState() ScreensaverState { m.stateMutex.RLock() defer m.stateMutex.RUnlock() return m.state.Screensaver } func (m *Manager) SubscribeScreensaver(id string) chan ScreensaverState { ch := make(chan ScreensaverState, 64) m.screensaverSubscribers.Store(id, ch) return ch } func (m *Manager) UnsubscribeScreensaver(id string) { if val, ok := m.screensaverSubscribers.LoadAndDelete(id); ok { close(val) } } func (m *Manager) NotifyScreensaverSubscribers() { state := m.GetScreensaverState() m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool { select { case ch <- state: default: } return true }) }