diff --git a/core/internal/server/network/backend_iwd.go b/core/internal/server/network/backend_iwd.go index a9b14c002..002e73c88 100644 --- a/core/internal/server/network/backend_iwd.go +++ b/core/internal/server/network/backend_iwd.go @@ -154,23 +154,7 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string] } if _, hasDevice := interfaces[iwdDeviceInterface]; hasDevice { b.devicePath = path - - if devProps, ok := interfaces[iwdDeviceInterface]; ok { - if nameVar, ok := devProps["Name"]; ok { - if name, ok := nameVar.Value().(string); ok { - b.stateMutex.Lock() - b.state.WiFiDevice = name - b.stateMutex.Unlock() - } - } - if poweredVar, ok := devProps["Powered"]; ok { - if powered, ok := poweredVar.Value().(bool); ok { - b.stateMutex.Lock() - b.state.WiFiEnabled = powered - b.stateMutex.Unlock() - } - } - } + b.applyDeviceProps(interfaces[iwdDeviceInterface]) } if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter { b.adapterPath = path @@ -193,6 +177,47 @@ func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string] return nil } +func (b *IWDBackend) applyDeviceProps(props map[string]dbus.Variant) { + if nameVar, ok := props["Name"]; ok { + if name, ok := nameVar.Value().(string); ok { + b.stateMutex.Lock() + b.state.WiFiDevice = name + b.stateMutex.Unlock() + } + } + if poweredVar, ok := props["Powered"]; ok { + if powered, ok := poweredVar.Value().(bool); ok { + b.stateMutex.Lock() + b.state.WiFiEnabled = powered + b.stateMutex.Unlock() + } + } +} + +// setDevice records a device that appeared at a new path, which is what +// happens when iwd restarts and recreates wlan0. It returns the path it +// replaced so the caller can move the signal match over. +func (b *IWDBackend) setDevice(path dbus.ObjectPath, props map[string]dbus.Variant) (prev dbus.ObjectPath, changed bool) { + if path == "" || path == b.devicePath { + return "", false + } + prev = b.devicePath + b.devicePath = path + b.applyDeviceProps(props) + return prev, true +} + +func (b *IWDBackend) clearDevice(path dbus.ObjectPath) bool { + if path == "" || path != b.devicePath { + return false + } + b.devicePath = "" + b.stateMutex.Lock() + b.state.WiFiEnabled = false + b.stateMutex.Unlock() + return true +} + func (b *IWDBackend) GetCurrentState() (*BackendState, error) { b.stateMutex.RLock() defer b.stateMutex.RUnlock() diff --git a/core/internal/server/network/backend_iwd_signals.go b/core/internal/server/network/backend_iwd_signals.go index e4541f5f2..60ea87625 100644 --- a/core/internal/server/network/backend_iwd_signals.go +++ b/core/internal/server/network/backend_iwd_signals.go @@ -26,12 +26,7 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error { b.conn.Signal(sigChan) if b.devicePath != "" { - err := b.conn.AddMatchSignal( - dbus.WithMatchObjectPath(b.devicePath), - dbus.WithMatchInterface(dbusPropertiesInterface), - dbus.WithMatchMember("PropertiesChanged"), - ) - if err != nil { + if err := b.addDeviceSignalMatch(b.devicePath); err != nil { return fmt.Errorf("failed to add device signal match: %w", err) } } @@ -86,6 +81,48 @@ func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error { ) } +func (b *IWDBackend) addDeviceSignalMatch(path dbus.ObjectPath) error { + return b.conn.AddMatchSignal( + dbus.WithMatchObjectPath(path), + dbus.WithMatchInterface(dbusPropertiesInterface), + dbus.WithMatchMember("PropertiesChanged"), + ) +} + +func (b *IWDBackend) removeDeviceSignalMatch(path dbus.ObjectPath) { + err := b.conn.RemoveMatchSignal( + dbus.WithMatchObjectPath(path), + dbus.WithMatchInterface(dbusPropertiesInterface), + dbus.WithMatchMember("PropertiesChanged"), + ) + if err != nil { + log.Debugf("Failed to remove iwd device signal match for %s: %v", path, err) + } +} + +func (b *IWDBackend) handleDeviceAdded(path dbus.ObjectPath, props map[string]dbus.Variant) bool { + prev, changed := b.setDevice(path, props) + if !changed { + return false + } + + if prev != "" { + b.removeDeviceSignalMatch(prev) + } + if err := b.addDeviceSignalMatch(path); err != nil { + log.Warnf("Failed to add iwd device signal match for %s: %v", path, err) + } + return true +} + +func (b *IWDBackend) handleDeviceRemoved(path dbus.ObjectPath) bool { + if !b.clearDevice(path) { + return false + } + b.removeDeviceSignalMatch(path) + return true +} + func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool { if path == "" || path == b.stationPath { return false @@ -139,6 +176,12 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { if len(sig.Body) >= 2 { path, _ := sig.Body[0].(dbus.ObjectPath) if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok { + // Device before Station: handleStationAdded reads Powered from devicePath. + if props, ok := interfaces[iwdDeviceInterface]; ok { + if b.handleDeviceAdded(path, props) && b.onStateChange != nil { + b.onStateChange() + } + } if _, ok := interfaces[iwdStationInterface]; ok { if b.handleStationAdded(path) && b.onStateChange != nil { b.onStateChange() @@ -159,17 +202,19 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { path, _ := sig.Body[0].(dbus.ObjectPath) if interfaces, ok := sig.Body[1].([]string); ok { for _, iface := range interfaces { - if iface == iwdStationInterface { + switch iface { + case iwdDeviceInterface: + if b.handleDeviceRemoved(path) && b.onStateChange != nil { + b.onStateChange() + } + case iwdStationInterface: if b.handleStationRemoved(path) && b.onStateChange != nil { b.onStateChange() } - break - } - if iface == iwdKnownNetworkInterface { + case iwdKnownNetworkInterface: if b.refreshWiFiNetworkState() && b.onStateChange != nil { b.onStateChange() } - break } } } diff --git a/core/internal/server/network/backend_iwd_test.go b/core/internal/server/network/backend_iwd_test.go index 1d0102d5d..2a1d883a6 100644 --- a/core/internal/server/network/backend_iwd_test.go +++ b/core/internal/server/network/backend_iwd_test.go @@ -460,3 +460,71 @@ func TestConnectAttempt_DoubleFinalization(t *testing.T) { assert.Equal(t, "bad-credentials", backend.state.LastError) backend.stateMutex.RUnlock() } + +func TestIWDBackend_SetDevice_FollowsRecreatedDevice(t *testing.T) { + backend, _ := NewIWDBackend() + err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{ + "/net/connman/iwd/0/3": { + iwdDeviceInterface: {"Name": dbus.MakeVariant("wlan0"), "Powered": dbus.MakeVariant(true)}, + iwdStationInterface: {}, + }, + }) + assert.NoError(t, err) + + // iwd restarts: the old objects go away and wlan0 comes back with a new ifindex. + assert.True(t, backend.handleStationRemoved("/net/connman/iwd/0/3")) + assert.True(t, backend.clearDevice("/net/connman/iwd/0/3")) + + prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{ + "Name": dbus.MakeVariant("wlan0"), + "Powered": dbus.MakeVariant(true), + }) + + assert.True(t, changed) + assert.Empty(t, prev) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath) + + enabled, _ := backend.GetWiFiEnabled() + assert.True(t, enabled, "Wi-Fi must be enabled again once the recreated device reports Powered") +} + +func TestIWDBackend_SetDevice_ReplacesPathWithoutRemovalSignal(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + prev, changed := backend.setDevice("/net/connman/iwd/0/7", map[string]dbus.Variant{ + "Powered": dbus.MakeVariant(true), + }) + + assert.True(t, changed) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), prev) + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/7"), backend.devicePath) +} + +func TestIWDBackend_SetDevice_SamePathIsNoop(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + _, changed := backend.setDevice("/net/connman/iwd/0/3", map[string]dbus.Variant{ + "Powered": dbus.MakeVariant(false), + }) + assert.False(t, changed) + + _, changed = backend.setDevice("", nil) + assert.False(t, changed) +} + +func TestIWDBackend_ClearDevice(t *testing.T) { + backend, _ := NewIWDBackend() + backend.devicePath = "/net/connman/iwd/0/3" + + assert.False(t, backend.clearDevice("/net/connman/iwd/0/9"), "unrelated path must be ignored") + assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/3"), backend.devicePath) + + assert.True(t, backend.clearDevice("/net/connman/iwd/0/3")) + assert.Empty(t, backend.devicePath) + + enabled, _ := backend.GetWiFiEnabled() + assert.False(t, enabled) + assert.ErrorContains(t, backend.SetWiFiEnabled(true), "no WiFi device available") +}