From 50e946917845d0f775d788a8d84124c1f1a1f31a Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Sun, 12 Jul 2026 17:06:08 -0400 Subject: [PATCH] Thread Safety Tool for bottom left. Transitioned navigator's prints to local logs via core's logger tool. Rolled back previous commit with bottom left status observers doing emit tools directly --- gui/__pycache__/__main__.cpython-312.pyc | Bin 4919 -> 4919 bytes gui/main_ui.py | 17 +++++++ gui/v2/infrastructure/ThreadSafetyTool.py | 21 ++++++++ gui/v2/infrastructure/navigator.py | 58 +++++++++++----------- pyproject.toml | 4 +- 5 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 gui/v2/infrastructure/ThreadSafetyTool.py diff --git a/gui/__pycache__/__main__.cpython-312.pyc b/gui/__pycache__/__main__.cpython-312.pyc index f1cb84d1770ec6e5fe3520b8e1ca7bb737c56780..e7a78bdea2340ba1457c71ce243625174fccd4df 100644 GIT binary patch delta 20 acmdn4wq1?;G%qg~0}$w%1#INj5e5J@VFY6U delta 20 acmdn4wq1?;G%qg~0}$w0d2Hm?5e5J@SOj1I diff --git a/gui/main_ui.py b/gui/main_ui.py index 7be4fd1..04add64 100755 --- a/gui/main_ui.py +++ b/gui/main_ui.py @@ -23,6 +23,9 @@ from core.errors.logger import logger as core_logger core_logger.propagate = False +from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool +mainthread = ThreadSafetyTool() + from gui.v2.infrastructure import orm from gui.v2.infrastructure.navigator import Navigator from gui.v2.infrastructure.setup_observers import setup_observers @@ -656,7 +659,21 @@ class CustomWindow(QMainWindow): self.status_label.setStyleSheet( f"color: rgb(0, 255, 255); font-size: {font_size}px;") + @mainthread # wrapper of ThreadSafetyTool def update_status(self, text, clear=False): + """ + Purpose: + Update the bottom left status bar + + Features: + Forced on the main Thread by ThreadSafetyTool + + Depends on: + v2.infrastructure.ThreadSafetyTool + + Called by: + v2.infrastructure.setup_observers (outside this class) + """ if text is None: self._set_status_font_size(16) self.status_label.setText('Status:') diff --git a/gui/v2/infrastructure/ThreadSafetyTool.py b/gui/v2/infrastructure/ThreadSafetyTool.py new file mode 100644 index 0000000..71d2082 --- /dev/null +++ b/gui/v2/infrastructure/ThreadSafetyTool.py @@ -0,0 +1,21 @@ +import sys +import time +from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel +from PyQt6.QtCore import QObject, pyqtSignal, Qt + +class ThreadSafetyTool(QObject): + """Marshal function calls to the main thread.""" + _signal = pyqtSignal(object, tuple, dict) + + def __init__(self): + super().__init__() + self._signal.connect(self._execute, Qt.ConnectionType.QueuedConnection) + + def _execute(self, func, args, kwargs): + func(*args, **kwargs) + + def __call__(self, func): + """Decorator that forces a function to run on the main thread.""" + def wrapper(*args, **kwargs): + self._signal.emit(func, args, kwargs) + return wrapper diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index a836bf7..d7b52d7 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -13,6 +13,7 @@ from gui.v2.infrastructure.page_registry import ( ) from gui.v2.workers.page_data_worker import PageDataWorker from gui.v2.actions.flags import is_fast_registration_enabled +from core.errors.logger import logger class Navigator(QObject): @@ -79,26 +80,25 @@ class Navigator(QObject): def _preload_one(self, name): if name in self._instances: - print(f"[preload] {name:30s} SKIP (already cached)", flush=True) + logger.debug(f"[preload] {name:30s} SKIP (already cached)") return False if name not in PAGE_REGISTRY: - print(f"[preload] {name:30s} SKIP (not in registry)", flush=True) + logger.debug(f"[preload] {name:30s} SKIP (not in registry)") return False module_path, class_name = PAGE_REGISTRY[name] - print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True) + logger.debug(f"[preload] {name:30s} LOAD {module_path}.{class_name}") t0 = time.perf_counter() try: page = self._instantiate(name) self.register_instance(name, page) dt_ms = (time.perf_counter() - t0) * 1000.0 - print(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, " - f"cached={len(self._instances)}, stack_size={self.page_stack.count()})", - flush=True) + logger.debug(f"[preload] {name:30s} OK ({dt_ms:6.1f} ms, " + f"cached={len(self._instances)}, stack_size={self.page_stack.count()})") return True except Exception as e: dt_ms = (time.perf_counter() - t0) * 1000.0 - print(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) " - f"{type(e).__name__}: {e}", flush=True) + logger.debug(f"[preload] {name:30s} FAIL ({dt_ms:6.1f} ms) " + f"{type(e).__name__}: {e}") traceback.print_exc() from core.errors.logger import logger as core_logger core_logger.warning(f"Background preload failed for '{name}': " @@ -121,29 +121,29 @@ class Navigator(QObject): self._preload_inflight = 0 self._preload_summary_emitted = False self._preload_t_start = time.perf_counter() - print(f"[preload] ============================================================", flush=True) - print(f"[preload] starting background preload: {self._preload_total} pages queued", flush=True) - print(f"[preload] order : {', '.join(self._preload_queue)}", flush=True) - print(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)", flush=True) + logger.debug(f"[preload] ============================================================") + logger.debug(f"[preload] starting background preload: {self._preload_total} pages queued") + logger.debug(f"[preload] order : {', '.join(self._preload_queue)}") + logger.debug(f"[preload] skipped : {', '.join(sorted(PRELOAD_SKIP))} (unsafe init)") if fast_mode: - print(f"[preload] fastmode: ON -> skipping regular-flow pages: " - f"{', '.join(skipped_regular) if skipped_regular else '(none)'}", flush=True) - print(f"[preload] gap : 250 ms between loads, 700 ms initial delay", flush=True) - print(f"[preload] ============================================================", flush=True) + logger.debug(f"[preload] fastmode: ON -> skipping regular-flow pages: " + f"{', '.join(skipped_regular) if skipped_regular else '(none)'}") + logger.debug(f"[preload] gap : 250 ms between loads, 700 ms initial delay") + logger.debug(f"[preload] ============================================================") self._preload_timer.start(700) def _preload_next(self): while self._preload_queue and self._preload_queue[0] in self._instances: skipped = self._preload_queue.pop(0) self._preload_skipped_runtime += 1 - print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True) + logger.debug(f"[preload] {skipped:30s} SKIP (user navigated here first)") if not self._preload_queue: self._maybe_emit_preload_summary() return self._preload_dispatched += 1 idx = self._preload_dispatched name = self._preload_queue.pop(0) - print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True) + logger.debug(f"[preload] ---- {idx}/{self._preload_total} ----") if name in ASYNC_PREPARE: self._dispatch_async_preload(name) else: @@ -165,7 +165,7 @@ class Navigator(QObject): def _dispatch_async_preload(self, name): module_path, class_name = PAGE_REGISTRY[name] - print(f"[preload] {name:30s} ASYNC {module_path}.{class_name}", flush=True) + logger.debug(f"[preload] {name:30s} ASYNC {module_path}.{class_name}") worker = PageDataWorker(name, self._prepare_page_data, name) worker.data_ready.connect(self._on_preload_data_ready) worker.failed.connect(self._on_preload_failed) @@ -177,22 +177,22 @@ class Navigator(QObject): def _on_preload_data_ready(self, name, payload): self._preload_inflight -= 1 if name in self._instances: - print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True) + logger.debug(f"[preload] {name:30s} SKIP (already cached, async result dropped)") else: try: page = self._instantiate(name, prepared=payload) self.register_instance(name, page) self._preload_done += 1 - print(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " - f"stack_size={self.page_stack.count()})", flush=True) + logger.debug(f"[preload] {name:30s} OK (async, cached={len(self._instances)}, " + f"stack_size={self.page_stack.count()})") except Exception as e: self._preload_failed += 1 - print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True) + logger.debug(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}") self._maybe_emit_preload_summary() def _on_preload_failed(self, name, error): self._preload_inflight -= 1 - print(f"[preload] {name:30s} FAIL (async prepare) {error}", flush=True) + logger.debug(f"[preload] {name:30s} FAIL (async prepare) {error}") if name not in self._instances: ok = self._preload_one(name) if ok: @@ -215,10 +215,10 @@ class Navigator(QObject): def _emit_preload_summary(self): total_s = time.perf_counter() - self._preload_t_start - print(f"[preload] ============================================================", flush=True) - print(f"[preload] complete: {self._preload_done} loaded, " + logger.debug(f"[preload] ============================================================") + logger.debug(f"[preload] complete: {self._preload_done} loaded, " f"{self._preload_failed} failed, " f"{self._preload_skipped_runtime} skipped (user beat us to it) " - f"in {total_s:.2f}s", flush=True) - print(f"[preload] cached pages now: {sorted(self._instances.keys())}", flush=True) - print(f"[preload] ============================================================", flush=True) + f"in {total_s:.2f}s") + logger.debug(f"[preload] cached pages now: {sorted(self._instances.keys())}") + logger.debug(f"[preload] ============================================================") diff --git a/pyproject.toml b/pyproject.toml index 628cb74..6dbce05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sp-hydra-veil-gui" -version = "2.4.6" +version = "2.4.7" authors = [ { name = "Simplified Privacy" }, ] @@ -12,7 +12,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", ] dependencies = [ - "sp-hydra-veil-core == 2.3.8", + "sp-hydra-veil-core == 2.3.9", "pyperclip ~= 1.9.0", "pyqt6 ~= 6.7.1", "qrcode[pil] ~= 8.2"