From 77757152d7a8b6f6d446678522f0d98d57944126 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:38:09 -0400 Subject: [PATCH] Add async background sync task management --- src/main.py | 5 +++++ src/seedpass/core/manager.py | 20 +++++++++++++++----- src/seedpass_gui/app.py | 3 +++ src/tests/test_unlock_sync.py | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/main.py b/src/main.py index 7a95e32..9b24a45 100644 --- a/src/main.py +++ b/src/main.py @@ -1056,6 +1056,7 @@ def display_menu( continue logging.info("Exiting the program.") print(colored("Exiting the program.", "green")) + getattr(password_manager, "cleanup", lambda: None)() password_manager.nostr_client.close_client_pool() sys.exit(0) if choice == "1": @@ -1259,6 +1260,7 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in print(colored("\nReceived shutdown signal. Exiting gracefully...", "yellow")) logging.info(f"Received shutdown signal: {sig}. Initiating graceful shutdown.") try: + getattr(password_manager, "cleanup", lambda: None)() password_manager.nostr_client.close_client_pool() logging.info("NostrClient closed successfully.") except Exception as exc: @@ -1277,6 +1279,7 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in logger.info("Program terminated by user via KeyboardInterrupt.") print(colored("\nProgram terminated by user.", "yellow")) try: + getattr(password_manager, "cleanup", lambda: None)() password_manager.nostr_client.close_client_pool() logging.info("NostrClient closed successfully.") except Exception as exc: @@ -1287,6 +1290,7 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in logger.error(f"A user-related error occurred: {e}", exc_info=True) print(colored(f"Error: {e}", "red")) try: + getattr(password_manager, "cleanup", lambda: None)() password_manager.nostr_client.close_client_pool() logging.info("NostrClient closed successfully.") except Exception as exc: @@ -1297,6 +1301,7 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in logger.error(f"An unexpected error occurred: {e}", exc_info=True) print(colored(f"Error: An unexpected error occurred: {e}", "red")) try: + getattr(password_manager, "cleanup", lambda: None)() password_manager.nostr_client.close_client_pool() logging.info("NostrClient closed successfully.") except Exception as exc: diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 13c1b68..3d6d7bb 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -1281,20 +1281,30 @@ class PasswordManager: async def _worker() -> None: try: - if hasattr(self, "nostr_client") and hasattr(self, "vault"): - self.attempt_initial_sync() - if hasattr(self, "sync_index_from_nostr"): - self.sync_index_from_nostr() + await self.attempt_initial_sync_async() + await self.sync_index_from_nostr_async() except Exception as exc: logger.warning(f"Background sync failed: {exc}") try: loop = asyncio.get_running_loop() except RuntimeError: - threading.Thread(target=lambda: asyncio.run(_worker()), daemon=True).start() + thread = threading.Thread( + target=lambda: asyncio.run(_worker()), daemon=True + ) + thread.start() + self._sync_task = thread else: self._sync_task = asyncio.create_task(_worker()) + def cleanup(self) -> None: + """Cancel any pending background sync task.""" + task = getattr(self, "_sync_task", None) + if isinstance(task, asyncio.Task) and not task.done(): + task.cancel() + elif isinstance(task, threading.Thread) and task.is_alive(): + task.join(timeout=0.1) + def start_background_relay_check(self) -> None: """Check relay health in a background thread.""" if ( diff --git a/src/seedpass_gui/app.py b/src/seedpass_gui/app.py index d0894ab..34f674d 100644 --- a/src/seedpass_gui/app.py +++ b/src/seedpass_gui/app.py @@ -195,6 +195,9 @@ class MainWindow(toga.Window): bus.unsubscribe("sync_started", self.sync_started) bus.unsubscribe("sync_finished", self.sync_finished) bus.unsubscribe("vault_locked", self.vault_locked) + manager = getattr(self.nostr, "_manager", None) + if manager is not None: + manager.cleanup() class EntryDialog(toga.Window): diff --git a/src/tests/test_unlock_sync.py b/src/tests/test_unlock_sync.py index a2852c4..58a62cb 100644 --- a/src/tests/test_unlock_sync.py +++ b/src/tests/test_unlock_sync.py @@ -1,4 +1,6 @@ import time +import asyncio +import warnings from types import SimpleNamespace from pathlib import Path import sys @@ -17,14 +19,15 @@ def test_unlock_triggers_sync(monkeypatch, tmp_path): pm.initialize_managers = lambda: None called = {"sync": False} - def fake_sync(self): + async def fake_sync(self): called["sync"] = True - monkeypatch.setattr(PasswordManager, "sync_index_from_nostr", fake_sync) + monkeypatch.setattr(PasswordManager, "sync_index_from_nostr_async", fake_sync) pm.unlock_vault("pw") pm.start_background_sync() time.sleep(0.05) + pm.cleanup() assert called["sync"] @@ -54,3 +57,29 @@ def test_quick_unlock_background_sync(monkeypatch, tmp_path): pm.exit_managed_account() assert called["bg"] + + +def test_start_background_sync_running_loop(monkeypatch): + pm = PasswordManager.__new__(PasswordManager) + pm.offline_mode = False + called = {"init": False, "sync": False} + + async def fake_attempt(self): + called["init"] = True + + async def fake_sync(self): + called["sync"] = True + + monkeypatch.setattr(PasswordManager, "attempt_initial_sync_async", fake_attempt) + monkeypatch.setattr(PasswordManager, "sync_index_from_nostr_async", fake_sync) + + async def runner(): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + pm.start_background_sync() + await asyncio.sleep(0.01) + assert not any(issubclass(wi.category, RuntimeWarning) for wi in w) + + asyncio.run(runner()) + pm.cleanup() + assert called["init"] and called["sync"]