From f0a7fb7da12d095b2cbb43620c158a77bfd3ca05 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:26:54 -0400 Subject: [PATCH] Queue background errors --- src/main.py | 1 + src/seedpass/core/manager.py | 19 +++++++ src/tests/test_background_error_reporting.py | 55 ++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/tests/test_background_error_reporting.py diff --git a/src/main.py b/src/main.py index 15eda46..ead323c 100644 --- a/src/main.py +++ b/src/main.py @@ -1064,6 +1064,7 @@ def display_menu( getattr(password_manager, "start_background_relay_check", lambda: None)() _display_live_stats(password_manager) while True: + getattr(password_manager, "poll_background_errors", lambda: None)() fp, parent_fp, child_fp = getattr( password_manager, "header_fingerprint_args", diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 77ff604..7a0d3fc 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -177,6 +177,7 @@ class PasswordManager: self.state_manager: Optional[StateManager] = None self.stats_manager: StatsManager = StatsManager() self.notifications: queue.Queue[Notification] = queue.Queue() + self.error_queue: queue.Queue[Exception] = queue.Queue() self._current_notification: Optional[Notification] = None self._notification_expiry: float = 0.0 @@ -307,6 +308,18 @@ class PasswordManager: return self._current_notification return None + def poll_background_errors(self) -> None: + """Process exceptions raised by background threads.""" + if not hasattr(self, "error_queue"): + return + while True: + try: + exc = self.error_queue.get_nowait() + except queue.Empty: + break + logger.warning("Background task failed: %s", exc) + self.notify(f"Background task failed: {exc}", level="WARNING") + def lock_vault(self) -> None: """Clear sensitive information from memory.""" if self.entry_manager is not None: @@ -1430,6 +1443,8 @@ class PasswordManager: await self.sync_index_from_nostr_async() except Exception as exc: logger.warning(f"Background sync failed: {exc}") + if hasattr(self, "error_queue"): + self.error_queue.put(exc) try: loop = asyncio.get_running_loop() @@ -1473,6 +1488,8 @@ class PasswordManager: ) except Exception as exc: logger.warning(f"Relay health check failed: {exc}") + if hasattr(self, "error_queue"): + self.error_queue.put(exc) self._relay_thread = threading.Thread(target=_worker, daemon=True) self._relay_thread.start() @@ -1489,6 +1506,8 @@ class PasswordManager: bus.publish("sync_finished", result) except Exception as exc: logging.error(f"Background vault sync failed: {exc}", exc_info=True) + if hasattr(self, "error_queue"): + self.error_queue.put(exc) try: loop = asyncio.get_running_loop() diff --git a/src/tests/test_background_error_reporting.py b/src/tests/test_background_error_reporting.py new file mode 100644 index 0000000..2f71aad --- /dev/null +++ b/src/tests/test_background_error_reporting.py @@ -0,0 +1,55 @@ +import logging +import queue + +import seedpass.core.manager as manager_module + + +def _make_pm(): + pm = manager_module.PasswordManager.__new__(manager_module.PasswordManager) + pm.offline_mode = False + pm.notifications = queue.Queue() + pm.error_queue = queue.Queue() + pm.notify = lambda msg, level="INFO": pm.notifications.put( + manager_module.Notification(msg, level) + ) + return pm + + +def test_start_background_sync_error(monkeypatch, caplog): + pm = _make_pm() + + async def failing_sync(*_args, **_kwargs): + raise RuntimeError("boom") + + monkeypatch.setattr(pm, "attempt_initial_sync_async", failing_sync) + monkeypatch.setattr(pm, "sync_index_from_nostr_async", failing_sync) + + pm.start_background_sync() + pm._sync_task.join(timeout=1) + + with caplog.at_level(logging.WARNING): + pm.poll_background_errors() + + note = pm.notifications.get_nowait() + assert "boom" in note.message + assert "boom" in caplog.text + + +def test_start_background_relay_check_error(monkeypatch, caplog): + pm = _make_pm() + + class DummyClient: + def check_relay_health(self, *_args, **_kwargs): + raise RuntimeError("relay boom") + + pm.nostr_client = DummyClient() + + pm.start_background_relay_check() + pm._relay_thread.join(timeout=1) + + with caplog.at_level(logging.WARNING): + pm.poll_background_errors() + + note = pm.notifications.get_nowait() + assert "relay boom" in note.message + assert "relay boom" in caplog.text