1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 12:52:06 -04:00
Files
bbedward 46aaf5ff77 fix(udev): avoid event loop termination
core: bump go to 1.26
2026-03-13 11:42:46 -04:00

238 lines
5.2 KiB
Go

package brightness
import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink"
)
const (
udevRecvBufSize = 8 * 1024 * 1024
udevMaxRetries = 5
udevBaseDelay = 2 * time.Second
udevMaxDelay = 60 * time.Second
)
type UdevMonitor struct {
stop chan struct{}
rescanMutex sync.Mutex
rescanTimer *time.Timer
rescanPending bool
}
func NewUdevMonitor(manager *Manager) *UdevMonitor {
m := &UdevMonitor{
stop: make(chan struct{}),
}
go m.run(manager)
return m
}
func (m *UdevMonitor) run(manager *Manager) {
matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
{Env: map[string]string{"SUBSYSTEM": "drm"}},
{Env: map[string]string{"SUBSYSTEM": "i2c"}},
},
}
if err := matcher.Compile(); err != nil {
log.Errorf("Failed to compile udev matcher: %v", err)
return
}
failures := 0
for {
if err := m.monitorLoop(manager, matcher); err != nil {
log.Errorf("Udev monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > udevMaxRetries {
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
return
}
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
return err
}
defer conn.Close()
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
log.Warnf("Failed to set udev socket receive buffer: %v", err)
}
events := make(chan netlink.UEvent)
errs := make(chan error)
conn.Monitor(events, errs, matcher)
log.Info("Udev monitor started for backlight/drm/i2c events")
for {
select {
case <-m.stop:
return nil
case err := <-errs:
return err
case event := <-events:
m.handleEvent(manager, event)
}
}
}
func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) {
subsystem := event.Env["SUBSYSTEM"]
devpath := event.Env["DEVPATH"]
if subsystem == "" || devpath == "" {
return
}
sysname := filepath.Base(devpath)
action := string(event.Action)
switch subsystem {
case "drm", "i2c":
m.handleDisplayEvent(manager, action, subsystem, sysname)
case "backlight":
m.handleBacklightEvent(manager, action, sysname)
}
}
func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) {
switch action {
case "add", "remove", "change":
log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname)
m.debouncedRescan(manager)
}
}
func (m *UdevMonitor) debouncedRescan(manager *Manager) {
m.rescanMutex.Lock()
defer m.rescanMutex.Unlock()
m.rescanPending = true
if m.rescanTimer != nil {
m.rescanTimer.Reset(2 * time.Second)
return
}
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
m.rescanMutex.Lock()
pending := m.rescanPending
m.rescanPending = false
m.rescanMutex.Unlock()
if !pending {
return
}
log.Debug("Executing debounced DDC rescan")
manager.Rescan()
})
}
func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) {
switch action {
case "change":
m.handleChange(manager, "backlight", sysname)
case "add", "remove":
log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname)
manager.Rescan()
}
}
func (m *UdevMonitor) handleChange(manager *Manager, subsystem, sysname string) {
deviceID := subsystem + ":" + sysname
if manager.sysfsBackend == nil {
return
}
brightnessPath := filepath.Join(manager.sysfsBackend.basePath, subsystem, sysname, "brightness")
data, err := os.ReadFile(brightnessPath)
if err != nil {
log.Debugf("Udev change event for %s but failed to read brightness: %v", deviceID, err)
return
}
brightness, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
log.Debugf("Failed to parse brightness for %s: %v", deviceID, err)
return
}
manager.handleUdevBrightnessChange(deviceID, brightness)
}
func (m *UdevMonitor) Close() {
close(m.stop)
}
func (m *Manager) handleUdevBrightnessChange(deviceID string, rawBrightness int) {
if m.sysfsBackend == nil {
return
}
dev, err := m.sysfsBackend.GetDevice(deviceID)
if err != nil {
log.Debugf("Udev event for unknown device %s: %v", deviceID, err)
return
}
percent := m.sysfsBackend.ValueToPercent(rawBrightness, dev, false)
m.stateMutex.Lock()
var found bool
for i, d := range m.state.Devices {
if d.ID != deviceID {
continue
}
found = true
if d.Current == rawBrightness {
m.stateMutex.Unlock()
return
}
m.state.Devices[i].Current = rawBrightness
m.state.Devices[i].CurrentPercent = percent
break
}
m.stateMutex.Unlock()
if !found {
log.Debugf("Udev event for device not in state: %s", deviceID)
return
}
log.Debugf("Udev brightness change: %s -> %d (%d%%)", deviceID, rawBrightness, percent)
m.broadcastDeviceUpdate(deviceID)
}