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() } }