diff --git a/gui/__main__.py b/gui/__main__.py index 9930cb3..92cca7a 100755 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -26,8 +26,15 @@ core_logger.propagate = False from gui.v2.infrastructure.navigator import Navigator from gui.v2.infrastructure.setup_observers import setup_observers from gui.v2.infrastructure.connection_manager import ConnectionManager -from gui.v2.infrastructure.page_registry import PRELOAD_ORDER, PRELOAD_SKIP +from gui.v2.infrastructure.page_registry import ( + PRELOAD_ORDER, + PRELOAD_SKIP, + ASYNC_PREPARE, + REGULAR_FLOW_ONLY, + PAGE_REGISTRY, +) from gui.v2.workers.worker_thread import WorkerThread +from gui.v2.workers.page_data_worker import PageDataWorker from gui.v2.ui.builders.bottom_section import create_bottom_section from gui.v2.ui.builders.top_section import create_top_section from gui.v2.ui.builders.system_tray import init_system_tray @@ -48,6 +55,7 @@ from gui.v2.actions.flags import ( has_shown_fast_mode_prompt, mark_fast_mode_prompt_shown, set_fast_mode_enabled, + is_fast_registration_enabled, check_first_launch, ) from gui.v2.actions.profile_data import ( @@ -170,6 +178,10 @@ class CustomWindow(QMainWindow): self._preload_done = 0 self._preload_failed = 0 self._preload_skipped_runtime = 0 + self._preload_dispatched = 0 + self._preload_inflight = 0 + self._preload_workers = {} + self._preload_summary_emitted = False self._preload_t_start = 0.0 self._preload_timer = QTimer(self) self._preload_timer.setSingleShot(True) @@ -202,15 +214,26 @@ class CustomWindow(QMainWindow): def _start_background_preload(self): import time self._preload_queue = [n for n in PRELOAD_ORDER if n not in PRELOAD_SKIP] + fast_mode = is_fast_registration_enabled(self.gui_config_file) + skipped_regular = [] + if fast_mode: + skipped_regular = [n for n in self._preload_queue if n in REGULAR_FLOW_ONLY] + self._preload_queue = [n for n in self._preload_queue if n not in REGULAR_FLOW_ONLY] self._preload_total = len(self._preload_queue) self._preload_done = 0 self._preload_failed = 0 self._preload_skipped_runtime = 0 + self._preload_dispatched = 0 + 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) + 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) self._preload_timer.start(700) @@ -221,19 +244,73 @@ class CustomWindow(QMainWindow): self._preload_skipped_runtime += 1 print(f"[preload] {skipped:30s} SKIP (user navigated here first)", flush=True) if not self._preload_queue: - self._emit_preload_summary() + self._maybe_emit_preload_summary() return - idx = self._preload_done + self._preload_failed + 1 + self._preload_dispatched += 1 + idx = self._preload_dispatched name = self._preload_queue.pop(0) print(f"[preload] ---- {idx}/{self._preload_total} ----", flush=True) - ok = self.navigator.preload(name) - if ok: - self._preload_done += 1 + if name in ASYNC_PREPARE: + self._dispatch_async_preload(name) else: - self._preload_failed += 1 + ok = self.navigator.preload(name) + if ok: + self._preload_done += 1 + else: + self._preload_failed += 1 if self._preload_queue: self._preload_timer.start(250) else: + self._maybe_emit_preload_summary() + + 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) + worker = PageDataWorker(name, module_path, class_name, self) + worker.data_ready.connect(self._on_preload_data_ready) + worker.failed.connect(self._on_preload_failed) + worker.finished.connect(lambda n=name: self._cleanup_preload_worker(n)) + self._preload_workers[name] = worker + self._preload_inflight += 1 + worker.start() + + def _on_preload_data_ready(self, name, payload): + self._preload_inflight -= 1 + if name in self.navigator._instances: + print(f"[preload] {name:30s} SKIP (already cached, async result dropped)", flush=True) + else: + try: + page = self.navigator._instantiate(name, prepared=payload) + self.navigator.register_instance(name, page) + self._preload_done += 1 + print(f"[preload] {name:30s} OK (async, cached={len(self.navigator._instances)}, " + f"stack_size={self.page_stack.count()})", flush=True) + except Exception as e: + self._preload_failed += 1 + print(f"[preload] {name:30s} FAIL (async build) {type(e).__name__}: {e}", flush=True) + 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) + if name not in self.navigator._instances: + ok = self.navigator.preload(name) + if ok: + self._preload_done += 1 + else: + self._preload_failed += 1 + self._maybe_emit_preload_summary() + + def _cleanup_preload_worker(self, name): + worker = self._preload_workers.pop(name, None) + if worker is not None: + worker.deleteLater() + + def _maybe_emit_preload_summary(self): + if self._preload_summary_emitted: + return + if not self._preload_queue and self._preload_inflight == 0: + self._preload_summary_emitted = True self._emit_preload_summary() def _emit_preload_summary(self): diff --git a/gui/v2/actions/flags.py b/gui/v2/actions/flags.py index a52add0..af16022 100755 --- a/gui/v2/actions/flags.py +++ b/gui/v2/actions/flags.py @@ -35,6 +35,13 @@ def mark_fast_mode_prompt_shown(gui_config_file): save_gui_config(gui_config_file, config) +def is_fast_registration_enabled(gui_config_file): + config = load_gui_config(gui_config_file) + if not config: + return False + return config.get("registrations", {}).get("fast_registration_enabled", False) + + def set_fast_mode_enabled(gui_config_file, enabled): config = load_gui_config(gui_config_file) if config is None: diff --git a/gui/v2/infrastructure/navigator.py b/gui/v2/infrastructure/navigator.py index 140fac9..4c54a8a 100755 --- a/gui/v2/infrastructure/navigator.py +++ b/gui/v2/infrastructure/navigator.py @@ -2,7 +2,7 @@ import importlib import time import traceback -from gui.v2.infrastructure.page_registry import PAGE_REGISTRY +from gui.v2.infrastructure.page_registry import PAGE_REGISTRY, ASYNC_PREPARE class Navigator: @@ -11,18 +11,29 @@ class Navigator: self.custom_window = custom_window self._instances = {} + def get_class(self, name): + module_path, class_name = PAGE_REGISTRY[name] + module = importlib.import_module(module_path) + return getattr(module, class_name) + + def _instantiate(self, name, prepared=None): + cls = self.get_class(name) + if name in ASYNC_PREPARE: + return cls(self.page_stack, self.custom_window, prepared=prepared) + return cls(self.page_stack, self.custom_window) + + def register_instance(self, name, page): + self.page_stack.addWidget(page) + self._instances[name] = page + def navigate(self, name): page = self._instances.get(name) if page is None: if name not in PAGE_REGISTRY: self._warn_not_migrated(name) return - module_path, class_name = PAGE_REGISTRY[name] - module = importlib.import_module(module_path) - cls = getattr(module, class_name) - page = cls(self.page_stack, self.custom_window) - self.page_stack.addWidget(page) - self._instances[name] = page + page = self._instantiate(name) + self.register_instance(name, page) self.page_stack.setCurrentIndex(self.page_stack.indexOf(page)) def preload(self, name): @@ -36,11 +47,8 @@ class Navigator: print(f"[preload] {name:30s} LOAD {module_path}.{class_name}", flush=True) t0 = time.perf_counter() try: - module = importlib.import_module(module_path) - cls = getattr(module, class_name) - page = cls(self.page_stack, self.custom_window) - self.page_stack.addWidget(page) - self._instances[name] = page + 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()})", diff --git a/gui/v2/infrastructure/page_registry.py b/gui/v2/infrastructure/page_registry.py index 8ce2b06..c74f069 100755 --- a/gui/v2/infrastructure/page_registry.py +++ b/gui/v2/infrastructure/page_registry.py @@ -38,6 +38,23 @@ PRELOAD_SKIP = { "ticket_prep", } +ASYNC_PREPARE = { + "settings", +} + +REGULAR_FLOW_ONLY = { + "protocol", + "location", + "hidetor", + "residential", + "tor", + "connection", + "screen", + "browser", + "wireguard", + "sync_screen", +} + PRELOAD_ORDER = [ "settings", "editor", diff --git a/gui/v2/ui/pages/Page.py b/gui/v2/ui/pages/Page.py index f9cd37c..d6b673c 100755 --- a/gui/v2/ui/pages/Page.py +++ b/gui/v2/ui/pages/Page.py @@ -19,6 +19,10 @@ class Page(QWidget): self.selected_residential = [] self.buttons = [] + @staticmethod + def prepare_data(custom_window): + return None + def add_selected_profile(self, profile): self.selected_profiles.clear() self.selected_wireguard.clear() diff --git a/gui/v2/ui/pages/settings_page.py b/gui/v2/ui/pages/settings_page.py index 7768db9..e5ab437 100755 --- a/gui/v2/ui/pages/settings_page.py +++ b/gui/v2/ui/pages/settings_page.py @@ -36,6 +36,7 @@ from core.errors.logger import logger as core_logger from gui.v2.actions.key_interpretation import interpret_key_results from gui.v2.actions.profile_order import normalize_profile_order +from gui.v2.infrastructure.page_registry import PAGE_REGISTRY from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.ui.pages.Page import Page from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup @@ -46,17 +47,21 @@ from gui.v2.ui.styles.styles import ( ) from gui.v2.ui.widgets.clickable_label import ClickableValueLabel from gui.v2.ui.widgets.terminal_widget import TerminalWidget +from gui.v2.workers.page_data_worker import PageDataWorker from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread from gui.v2.workers.worker_thread import WorkerThread class Settings(Page): - def __init__(self, page_stack, main_window, parent=None): + def __init__(self, page_stack, main_window, parent=None, prepared=None): super().__init__("Settings", page_stack, main_window, parent) self.font_style = f"font-family: '{self.custom_window.open_sans_family}';" self.update_status = main_window self.update_logging = main_window + self._prepared = prepared if prepared is not None else self.prepare_data(main_window) + self._settings_refresh_seq = 0 + self._settings_workers = set() self.button_reverse.setVisible(True) self.button_reverse.setEnabled(True) self.button_reverse.clicked.connect(self.reverse) @@ -64,6 +69,52 @@ class Settings(Page): self.title.setText("Settings") self.setup_ui() + @staticmethod + def prepare_data(custom_window): + profiles = ProfileController.get_all() + profile_order = normalize_profile_order( + getattr(custom_window, 'gui_config_file', None), profiles.keys()) + current_connection = custom_window.get_current_connection() + try: + endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled() + except Exception: + endpoint_verification_enabled = False + return { + "profiles": profiles, + "profile_order": profile_order, + "current_connection": current_connection, + "endpoint_verification_enabled": endpoint_verification_enabled, + "systemwide_enabled": Settings._read_systemwide_enabled(), + "bwrap_enabled": Settings._read_bwrap_enabled(), + "gui_config": custom_window._load_gui_config(), + "ticket_failure": custom_window.get_ticket_verification_failure(), + } + + def _prepared_value(self, key, loader): + if self._prepared is not None and key in self._prepared: + return self._prepared[key] + return loader() + + @staticmethod + def _read_systemwide_enabled(): + try: + privilege_policy = PolicyController.get('privilege') + if privilege_policy is not None: + return PolicyController.is_instated(privilege_policy) + except Exception: + pass + return False + + @staticmethod + def _read_bwrap_enabled(): + try: + capability_policy = PolicyController.get('capability') + if capability_policy is not None: + return PolicyController.is_instated(capability_policy) + except Exception: + pass + return False + def setup_ui(self): main_container = QWidget(self) main_container.setGeometry(0, 0, 800, 520) @@ -198,11 +249,13 @@ class Settings(Page): return page def create_delete_profile_buttons(self): - profiles = ProfileController.get_all() + profiles = self._prepared_value("profiles", ProfileController.get_all) - profile_ids = normalize_profile_order( - getattr(self.update_status, 'gui_config_file', None), - profiles.keys()) + profile_ids = self._prepared_value( + "profile_order", + lambda: normalize_profile_order( + getattr(self.update_status, 'gui_config_file', None), + profiles.keys())) for index, profile_id in enumerate(profile_ids): profile = profiles[profile_id] @@ -600,7 +653,7 @@ class Settings(Page): def update_debug_profile_list(self): self.debug_profile_selector.clear() - profiles = ProfileController.get_all() + profiles = self._prepared_value("profiles", ProfileController.get_all) system_profiles = {pid: profile for pid, profile in profiles.items( ) if isinstance(profile, SystemProfile)} @@ -904,7 +957,7 @@ class Settings(Page): self.profile_selector = QComboBox() self.profile_selector.setStyleSheet(self.get_combobox_style()) - profiles = ProfileController.get_all() + profiles = self._prepared_value("profiles", ProfileController.get_all) if profiles: for profile_id, profile in profiles.items(): self.profile_selector.addItem( @@ -1039,7 +1092,7 @@ class Settings(Page): self.verification_profile_selector = QComboBox() self.verification_profile_selector.setStyleSheet( self.get_combobox_style()) - profiles = ProfileController.get_all() + profiles = self._prepared_value("profiles", ProfileController.get_all) if profiles: for profile_id, profile in profiles.items(): self.verification_profile_selector.addItem( @@ -1134,7 +1187,9 @@ class Settings(Page): self.endpoint_verification_checkbox = QCheckBox(page) self.endpoint_verification_checkbox.setGeometry(180, 415, 30, 30) self.endpoint_verification_checkbox.setChecked( - ConfigurationController.get_endpoint_verification_enabled()) + self._prepared_value( + "endpoint_verification_enabled", + ConfigurationController.get_endpoint_verification_enabled)) self.endpoint_verification_checkbox.setStyleSheet( self.get_checkbox_style()) self.endpoint_verification_checkbox.show() @@ -1295,6 +1350,31 @@ class Settings(Page): def showEvent(self, event): super().showEvent(event) + self._settings_refresh_seq += 1 + seq = self._settings_refresh_seq + module_path, class_name = PAGE_REGISTRY["settings"] + worker = PageDataWorker( + "settings", module_path, class_name, self.custom_window) + worker.data_ready.connect( + lambda name, payload, s=seq: self._on_settings_data_ready(payload, s)) + worker.failed.connect( + lambda name, error: self._on_settings_data_failed(error)) + worker.finished.connect( + lambda w=worker: self._cleanup_settings_worker(w)) + self._settings_workers.add(worker) + worker.start() + + def _on_settings_data_failed(self, error): + core_logger.warning(f"Settings data refresh failed: {error}") + + def _cleanup_settings_worker(self, worker): + self._settings_workers.discard(worker) + worker.deleteLater() + + def _on_settings_data_ready(self, payload, seq): + if seq != self._settings_refresh_seq: + return + self._prepared = payload current_index = self.content_layout.currentIndex() @@ -1407,7 +1487,7 @@ class Settings(Page): layout = QGridLayout(widget) layout.setSpacing(10) - profiles = ProfileController.get_all() + profiles = self._prepared_value("profiles", ProfileController.get_all) total_profiles = len(profiles) session_profiles = sum(1 for p in profiles.values() if isinstance(p, SessionProfile)) @@ -1439,7 +1519,8 @@ class Settings(Page): config_path = Constants.HV_CONFIG_HOME - current_connection = self.update_status.get_current_connection() + current_connection = self._prepared_value( + "current_connection", self.update_status.get_current_connection) if current_connection is not None: current_connection = current_connection.capitalize() @@ -1697,7 +1778,8 @@ class Settings(Page): inventory_layout.addWidget(self.refresh_tickets_button) layout.addWidget(inventory_group) - saved_failure = self.update_status.get_ticket_verification_failure() + saved_failure = self._prepared_value( + "ticket_failure", self.update_status.get_ticket_verification_failure) has_failure = saved_failure is not None recovery_group = QGroupBox("Verification Failure Recovery") @@ -1954,7 +2036,8 @@ class Settings(Page): def load_logs_settings(self) -> None: try: - config = self.update_status._load_gui_config() + config = self._prepared_value( + "gui_config", self.update_status._load_gui_config) if config and "logging" in config: self.enable_gui_logging.setChecked( config["logging"]["gui_logging_enabled"]) @@ -2103,7 +2186,8 @@ class Settings(Page): layout.addLayout(button_layout) layout.addStretch() - self.load_systemwide_settings() + self.load_systemwide_settings( + self._prepared_value("systemwide_enabled", self._read_systemwide_enabled)) return page def create_bwrap_page(self): @@ -2155,17 +2239,13 @@ class Settings(Page): layout.addLayout(button_layout) layout.addStretch() - self.load_bwrap_settings() + self.load_bwrap_settings( + self._prepared_value("bwrap_enabled", self._read_bwrap_enabled)) return page - def load_systemwide_settings(self): - enabled = False - try: - privilege_policy = PolicyController.get('privilege') - if privilege_policy is not None: - enabled = PolicyController.is_instated(privilege_policy) - except Exception: - enabled = False + def load_systemwide_settings(self, enabled=None): + if enabled is None: + enabled = self._read_systemwide_enabled() self.systemwide_toggle.setChecked(enabled) if enabled: self.systemwide_status_value.setText("Enabled") @@ -2202,14 +2282,9 @@ class Settings(Page): self.systemwide_status_value.setStyleSheet( f"color: red; font-size: 14px; {self.font_style}") - def load_bwrap_settings(self): - enabled = False - try: - capability_policy = PolicyController.get('capability') - if capability_policy is not None: - enabled = PolicyController.is_instated(capability_policy) - except Exception: - enabled = False + def load_bwrap_settings(self, enabled=None): + if enabled is None: + enabled = self._read_bwrap_enabled() self.bwrap_toggle.setChecked(enabled) if enabled: self.bwrap_status_value.setText("Enabled") @@ -2248,7 +2323,8 @@ class Settings(Page): def load_registrations_settings(self) -> None: try: - config = self.update_status._load_gui_config() + config = self._prepared_value( + "gui_config", self.update_status._load_gui_config) if config and "registrations" in config: registrations = config["registrations"] auto_sync = registrations.get("auto_sync_enabled", False) diff --git a/gui/v2/workers/page_data_worker.py b/gui/v2/workers/page_data_worker.py new file mode 100755 index 0000000..630341e --- /dev/null +++ b/gui/v2/workers/page_data_worker.py @@ -0,0 +1,24 @@ +import importlib + +from PyQt6.QtCore import QThread, pyqtSignal + + +class PageDataWorker(QThread): + data_ready = pyqtSignal(str, object) + failed = pyqtSignal(str, str) + + def __init__(self, name, module_path, class_name, custom_window): + super().__init__() + self.name = name + self.module_path = module_path + self.class_name = class_name + self.custom_window = custom_window + + def run(self): + try: + module = importlib.import_module(self.module_path) + cls = getattr(module, self.class_name) + payload = cls.prepare_data(self.custom_window) + self.data_ready.emit(self.name, payload) + except Exception as error: + self.failed.emit(self.name, f"{type(error).__name__}: {error}")