From 23b4aaf2a5e3008bf230cfaa86027b1ccbaedbb7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 03:25:01 -0700 Subject: [PATCH] raylib networking: remove locking on UI thread (#36063) * use callback queue to make this thread safe and remove locks (which lag ui thread?) * woah this works * no more lock! * always run signal handler and store callbacks, like qt * debug * more * okay not for now * combine _get_connections and _connection_by_ssid, closer to qt and not an explosion of GetSettings dbus calls * debug * try this * skip * len * skip hidden networks * actually slower * stash * back to 8929f37d495a524d4a996d66b82d4a947fbf4f1c * clean up --- system/ui/lib/wifi_manager.py | 26 +++++++----- system/ui/widgets/network.py | 74 ++++++++++++++++------------------- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 96940c2b3b..58181157c0 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -133,12 +133,12 @@ class WifiManager: # State self._connecting_to_ssid: str = "" self._last_network_update: float = 0.0 + self._callback_queue: list[Callable] = [] # Callbacks - # TODO: implement a callback queue to avoid blocking UI thread self._need_auth: Callable[[str], None] | None = None self._activated: Callable[[], None] | None = None - self._forgotten: Callable[[str], None] | None = None + self._forgotten: Callable[[], None] | None = None self._networks_updated: Callable[[list[Network]], None] | None = None self._disconnected: Callable[[], None] | None = None @@ -154,7 +154,7 @@ class WifiManager: def set_callbacks(self, need_auth: Callable[[str], None], activated: Callable[[], None] | None, - forgotten: Callable[[str], None], + forgotten: Callable[[], None], networks_updated: Callable[[list[Network]], None], disconnected: Callable[[], None]): self._need_auth = need_auth @@ -163,6 +163,15 @@ class WifiManager: self._networks_updated = networks_updated self._disconnected = disconnected + def _enqueue_callback(self, cb: Callable, *args): + self._callback_queue.append(lambda: cb(*args)) + + def process_callbacks(self): + # Call from UI thread to run any pending callbacks + to_run, self._callback_queue = self._callback_queue, [] + for cb in to_run: + cb() + def set_active(self, active: bool): self._active = active @@ -187,7 +196,6 @@ class WifiManager: with self._conn_monitor.filter(rule, bufsize=SIGNAL_QUEUE_SIZE) as q: while not self._exit: - # TODO: always run, and ensure callbacks don't block UI thread if not self._active: time.sleep(1) continue @@ -204,19 +212,19 @@ class WifiManager: if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): self.forget_connection(self._connecting_to_ssid, block=True) if self._need_auth is not None: - self._need_auth(self._connecting_to_ssid) + self._enqueue_callback(self._need_auth, self._connecting_to_ssid) self._connecting_to_ssid = "" elif new_state == NMDeviceState.ACTIVATED: if self._activated is not None: self._update_networks() - self._activated() + self._enqueue_callback(self._activated) self._connecting_to_ssid = "" elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: self._connecting_to_ssid = "" if self._disconnected is not None: - self._disconnected() + self._enqueue_callback(self._disconnected) def _network_scanner(self): self._wait_for_wifi_device() @@ -324,7 +332,7 @@ class WifiManager: if self._forgotten is not None: self._update_networks() - self._forgotten(ssid) + self._enqueue_callback(self._forgotten) if block: worker() @@ -400,7 +408,7 @@ class WifiManager: self._networks = networks if self._networks_updated is not None: - self._networks_updated(self._networks) + self._enqueue_callback(self._networks_updated, self._networks) def __del__(self): self.stop() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 6f5a00015b..3e6317a49c 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,6 +1,5 @@ from enum import IntEnum from functools import partial -from threading import Lock from typing import cast import pyray as rl @@ -50,7 +49,6 @@ class WifiManagerUI(Widget): self._networks: list[Network] = [] self._networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {} - self._lock = Lock() self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self.wifi_manager.set_callbacks(need_auth=self._on_need_auth, @@ -71,21 +69,22 @@ class WifiManagerUI(Widget): gui_app.texture(icon, ICON_SIZE, ICON_SIZE) def _render(self, rect: rl.Rectangle): - with self._lock: - if not self._networks: - gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - return - - if self.state == UIState.NEEDS_AUTH and self._state_network: - self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") - self.keyboard.reset() - gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) - elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: - self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') - self._confirm_dialog.reset() - gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) - else: - self._draw_network_list(rect) + self.wifi_manager.process_callbacks() + + if not self._networks: + gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + return + + if self.state == UIState.NEEDS_AUTH and self._state_network: + self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") + self.keyboard.reset() + gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) + elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: + self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') + self._confirm_dialog.reset() + gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) + else: + self._draw_network_list(rect) def _on_password_entered(self, network: Network, result: int): if result == 1: @@ -211,36 +210,31 @@ class WifiManagerUI(Widget): self.wifi_manager.forget_connection(network.ssid) def _on_network_updated(self, networks: list[Network]): - with self._lock: - self._networks = networks - for n in self._networks: - self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, - button_style=ButtonStyle.NO_EFFECT) - self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, - font_size=45) + self._networks = networks + for n in self._networks: + self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, + button_style=ButtonStyle.NO_EFFECT) + self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, + font_size=45) def _on_need_auth(self, ssid): - with self._lock: - network = next((n for n in self._networks if n.ssid == ssid), None) - if network: - self.state = UIState.NEEDS_AUTH - self._state_network = network - self._password_retry = True + network = next((n for n in self._networks if n.ssid == ssid), None) + if network: + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = True def _on_activated(self): - with self._lock: - if self.state == UIState.CONNECTING: - self.state = UIState.IDLE + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE - def _on_forgotten(self, ssid): - with self._lock: - if self.state == UIState.FORGETTING: - self.state = UIState.IDLE + def _on_forgotten(self): + if self.state == UIState.FORGETTING: + self.state = UIState.IDLE def _on_disconnected(self): - with self._lock: - if self.state == UIState.CONNECTING: - self.state = UIState.IDLE + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE def main():