From 496ae85f6763757b0f122400da408c45a99ab3b3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 25 Feb 2026 19:30:02 -0800 Subject: [PATCH] WifiManager: guard init_wifi_state (#37413) * failing test * fix * rename * better --- .../ui/lib/tests/test_handle_state_change.py | 27 +++++++++++++++++++ system/ui/lib/wifi_manager.py | 16 ++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/system/ui/lib/tests/test_handle_state_change.py b/system/ui/lib/tests/test_handle_state_change.py index 1365b84a8..30d838542 100644 --- a/system/ui/lib/tests/test_handle_state_change.py +++ b/system/ui/lib/tests/test_handle_state_change.py @@ -347,6 +347,33 @@ class TestThreadRaces: assert wm._wifi_state.ssid == "B" assert wm._wifi_state.status == ConnectStatus.CONNECTING + def test_init_wifi_state_race_user_tap_during_dbus(self, mocker): + """User taps B while _init_wifi_state's DBus calls are in flight. + + _init_wifi_state runs from set_active(True) or worker error paths. It does + 2 DBus calls (device State property + _get_active_wifi_connection) then + unconditionally writes _wifi_state. If the user taps a network during those + calls, _set_connecting("B") is overwritten with stale NM ground truth. + """ + wm = _make_wm(mocker, connections={"A": "/path/A", "B": "/path/B"}) + wm._wifi_device = "/dev/wifi0" + wm._router_main = mocker.MagicMock() + + state_reply = mocker.MagicMock() + state_reply.body = [('u', NMDeviceState.ACTIVATED)] + wm._router_main.send_and_get_reply.return_value = state_reply + + def user_taps_b_during_dbus(*args, **kwargs): + wm._set_connecting("B") + return ("/path/A", {}) + + wm._get_active_wifi_connection.side_effect = user_taps_b_during_dbus + + wm._init_wifi_state() + + assert wm._wifi_state.ssid == "B" + assert wm._wifi_state.status == ConnectStatus.CONNECTING + # --------------------------------------------------------------------------- # Full sequences (NM signal order from real devices) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 81c4ec051..6e39fdf52 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -197,7 +197,7 @@ class WifiManager: self._networks_updated: list[Callable[[list[Network]], None]] = [] self._disconnected: list[Callable[[], None]] = [] - self._lock = threading.Lock() + self._scan_lock = threading.Lock() self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True) self._state_thread = threading.Thread(target=self._monitor_state, daemon=True) self._initialize() @@ -227,6 +227,8 @@ class WifiManager: cloudlog.warning("No WiFi device found") return + epoch = self._user_epoch + dev_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_DEVICE_IFACE) dev_state = self._router_main.send_and_get_reply(Properties(dev_addr).get('State')).body[0][1] @@ -239,6 +241,10 @@ class WifiManager: conn_path, _ = self._get_active_wifi_connection() if conn_path: wifi_state.ssid = next((s for s, p in self._connections.items() if p == conn_path), None) + + if self._user_epoch != epoch: + return + self._wifi_state = wifi_state if block: @@ -281,11 +287,13 @@ class WifiManager: @property def connecting_to_ssid(self) -> str | None: - return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTING else None + wifi_state = self._wifi_state + return wifi_state.ssid if wifi_state.status == ConnectStatus.CONNECTING else None @property def connected_ssid(self) -> str | None: - return self._wifi_state.ssid if self._wifi_state.status == ConnectStatus.CONNECTED else None + wifi_state = self._wifi_state + return wifi_state.ssid if wifi_state.status == ConnectStatus.CONNECTED else None @property def tethering_password(self) -> str: @@ -822,7 +830,7 @@ class WifiManager: return def worker(): - with self._lock: + with self._scan_lock: if self._wifi_device is None: cloudlog.warning("No WiFi device found") return