From bb3e3adaab7b24c79697cb12a27ae76f699a0564 Mon Sep 17 00:00:00 2001 From: Evgeny Zemtsov Date: Mon, 19 Oct 2026 21:39:56 +0200 Subject: [PATCH] network(networkmanager): rebuild the Wi-Fi list once per scan (#3818) --- .../server/network/backend_networkmanager.go | 5 +++ .../network/backend_networkmanager_signals.go | 45 ++++++++++++++++++- .../backend_networkmanager_signals_test.go | 32 +++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/core/internal/server/network/backend_networkmanager.go b/core/internal/server/network/backend_networkmanager.go index 3b51f654d..fc2354892 100644 --- a/core/internal/server/network/backend_networkmanager.go +++ b/core/internal/server/network/backend_networkmanager.go @@ -4,6 +4,7 @@ import ( "fmt" "maps" "sync" + "time" "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/Wifx/gonetworkmanager/v2" @@ -80,6 +81,10 @@ type NetworkManagerBackend struct { sigWG sync.WaitGroup stopChan chan struct{} + wifiRefreshTimer *time.Timer + wifiRefreshMu sync.Mutex + wifiRefreshWG sync.WaitGroup + secretAgent *SecretAgent promptBroker PromptBroker diff --git a/core/internal/server/network/backend_networkmanager_signals.go b/core/internal/server/network/backend_networkmanager_signals.go index 5c5717180..db777e28f 100644 --- a/core/internal/server/network/backend_networkmanager_signals.go +++ b/core/internal/server/network/backend_networkmanager_signals.go @@ -1,6 +1,8 @@ package network import ( + "time" + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/Wifx/gonetworkmanager/v2" "github.com/godbus/dbus/v5" @@ -10,6 +12,8 @@ const ( dbusNMSettingsPath = "/org/freedesktop/NetworkManager/Settings" dbusNMSettingsInterface = "org.freedesktop.NetworkManager.Settings" dbusNMSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection" + + wifiNetworksUpdateDelay = 500 * time.Millisecond ) func (b *NetworkManagerBackend) startSignalPump() error { @@ -287,7 +291,8 @@ func (b *NetworkManagerBackend) stopSignalPump() { } b.sigWG.Wait() - + b.cancelWiFiNetworksUpdate() + b.wifiRefreshWG.Wait() } func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) { @@ -537,7 +542,7 @@ func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant needsStateUpdate = true needsNetworkUpdate = true case "AccessPoints": - needsNetworkUpdate = true + b.scheduleWiFiNetworksUpdate() default: continue } @@ -547,6 +552,7 @@ func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant b.updateWiFiState() } if needsNetworkUpdate { + b.cancelWiFiNetworksUpdate() b.updateWiFiNetworks() } if needsStateUpdate || needsNetworkUpdate { @@ -556,6 +562,41 @@ func (b *NetworkManagerBackend) handleWiFiChange(changes map[string]dbus.Variant } } +// A scan announces each new access point on its own, so rebuild the list +// once per burst instead of once per access point. +func (b *NetworkManagerBackend) scheduleWiFiNetworksUpdate() { + b.wifiRefreshMu.Lock() + defer b.wifiRefreshMu.Unlock() + + if b.wifiRefreshTimer != nil { + return + } + b.wifiRefreshWG.Add(1) + b.wifiRefreshTimer = time.AfterFunc(wifiNetworksUpdateDelay, func() { + defer b.wifiRefreshWG.Done() + + b.wifiRefreshMu.Lock() + b.wifiRefreshTimer = nil + b.wifiRefreshMu.Unlock() + + b.updateWiFiNetworks() + if b.onStateChange != nil { + b.onStateChange() + } + }) +} + +func (b *NetworkManagerBackend) cancelWiFiNetworksUpdate() { + b.wifiRefreshMu.Lock() + defer b.wifiRefreshMu.Unlock() + + if b.wifiRefreshTimer == nil || !b.wifiRefreshTimer.Stop() { + return + } + b.wifiRefreshTimer = nil + b.wifiRefreshWG.Done() +} + func (b *NetworkManagerBackend) handleAccessPointChange(changes map[string]dbus.Variant) { _, hasStrength := changes["Strength"] if !hasStrength { diff --git a/core/internal/server/network/backend_networkmanager_signals_test.go b/core/internal/server/network/backend_networkmanager_signals_test.go index b9147f5c2..e882fa5ff 100644 --- a/core/internal/server/network/backend_networkmanager_signals_test.go +++ b/core/internal/server/network/backend_networkmanager_signals_test.go @@ -1,7 +1,9 @@ package network import ( + "sync/atomic" "testing" + "time" mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" "github.com/Wifx/gonetworkmanager/v2" @@ -98,3 +100,33 @@ func TestNetworkManagerBackend_HandleDeviceChange_UnmanagedRefreshesHotspotState defer backend.stateMutex.RUnlock() assert.False(t, backend.state.HotspotAvailable, "availability must be recomputed when the only AP-capable radio becomes unmanaged") } + +func TestNetworkManagerBackend_HandleWiFiChange_CoalescesAccessPointBurst(t *testing.T) { + mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) + mockWiFi := mock_gonetworkmanager.NewMockDeviceWireless(t) + + backend, err := NewNetworkManagerBackend(mockNM) + assert.NoError(t, err) + backend.settings = emptySettingsMock(t) + backend.wifiDevice = mockWiFi + backend.wifiDev = mockWiFi + + var stateChangeCalls atomic.Int32 + backend.onStateChange = func() { + stateChangeCalls.Add(1) + } + + mockWiFi.EXPECT().GetAccessPoints().Return([]gonetworkmanager.AccessPoint{}, nil).Once() + + changes := map[string]dbus.Variant{ + "AccessPoints": dbus.MakeVariant([]dbus.ObjectPath{}), + } + for range 5 { + backend.handleWiFiChange(changes) + } + assert.Zero(t, stateChangeCalls.Load(), "the list must not be rebuilt per access point") + + assert.Eventually(t, func() bool { + return stateChangeCalls.Load() == 1 + }, 4*wifiNetworksUpdateDelay, 10*time.Millisecond) +}