diff --git a/gui/__main__.py b/gui/__main__.py index cfd6510..18b12d4 100755 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -10,6 +10,7 @@ from core.controllers.SyncController import new_sync from core.controllers.ConfigurationController import ConfigurationController from essentials.observers.ConnectionObserver import ConnectionObserver from core.observers.ClientObserver import ClientObserver +from gui.v2.actions.database_health import GuiStorageDatabaseError, validate_storage_database, has_required_sync_data # generic import os @@ -121,12 +122,14 @@ if not assets_folder_setup(): # INITIALIZE DATABASE # ============================================================================ lock_file = Path(f"{Constants.HV_DATA_HOME}/deleted_db.lock") +system_path = get_path() +database_path = system_path / "storage.db" +main_db_exists = False logger.info("[DB MANAGEMENT] Starting DB init..") try: - # Does the database exist? - system_path = get_path() - database_path = system_path / "storage.db" main_db_exists = does_it_exist(database_path) + if main_db_exists: + validate_storage_database(database_path) # Setup operations on the main DB which create it init_session() # (engine, Session, _session all initialized from session_management) @@ -134,9 +137,14 @@ try: # does the version checker table exist? version_table_exists = does_db_version_table_exist() -except: - logger.error("Critical Error with database initialization!") - sys.exit() +except GuiStorageDatabaseError as error: + logger.error(f"[DB MANAGEMENT] Critical storage database error during initialization: {error}") + close_session() + recovery_dialog(str(error), "Unreadable") +except Exception as error: + logger.error(f"[DB MANAGEMENT] Critical Error with database initialization: {error}") + close_session() + recovery_dialog("HydraVeil could not initialize the local storage database. Please move or reset storage.db, then restart.", "Startup Error") # ============================================================================ # LEGACY DATABASE CHECKS @@ -248,6 +256,14 @@ did_it_insert = insert_new_version(session, Constants.DB_VERSION_THIS_APP_WANTS) if not did_it_insert: logger.error(f"[DB MANAGEMENT] Critical Failure with updating/inserting the new DB version into the database.") +try: + sync_data_missing = not has_required_sync_data(database_path) +except GuiStorageDatabaseError as error: + logger.error(f"[DB MANAGEMENT] Critical storage database error during synced data check: {error}") + close_session() + recovery_dialog(str(error), "Unreadable") + sync_data_missing = True + # Load GUI either way: from gui.main_ui import start_ui @@ -256,7 +272,10 @@ if migration_happened: force_sync = True clear_sync_cache() else: - force_sync = False + force_sync = sync_data_missing + +if sync_data_missing: + logger.info("[DB MANAGEMENT] Required synced data is missing. GUI will prompt for sync.") # if we deleted their DB, we want to force sync, if lock_file.exists(): @@ -266,5 +285,3 @@ if lock_file.exists(): # Start GUI either way: logger.info(f"[DB MANAGEMENT] Starting GUI..") start_ui(force_sync) - - diff --git a/gui/main_ui.py b/gui/main_ui.py index 2418024..8bdd19a 100755 --- a/gui/main_ui.py +++ b/gui/main_ui.py @@ -59,6 +59,7 @@ from gui.v2.actions.profile_data import ( write_profile_data, clear_profile_data, ) +from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.actions.ticket_failure import ( save_ticket_verification_failure, get_ticket_verification_failure, @@ -303,6 +304,18 @@ class CustomWindow(QMainWindow): if issubclass(identifier, UnknownConnectionTypeError): self.setup_popup() os.execv(sys.executable, [sys.executable] + sys.argv) + elif issubclass(identifier, GuiStorageDatabaseError): + core_logger.error( + f"Storage database error:\n" + f"Type: {identifier.__name__}\n" + f"Value: {str(message)}\n" + f"Traceback:\n{''.join(traceback.format_tb(trace))}" + ) + QMessageBox.critical( + self, + "Database Error", + "The local storage database could not be read. Restart HydraVeil and use the database recovery options.", + ) else: config = self._load_gui_config() if config and config["logging"]["gui_logging_enabled"] == True: diff --git a/gui/v2/actions/database_health.py b/gui/v2/actions/database_health.py new file mode 100755 index 0000000..7b8c4c4 --- /dev/null +++ b/gui/v2/actions/database_health.py @@ -0,0 +1,89 @@ +import sqlite3 +from pathlib import Path + + +class GuiStorageDatabaseError(Exception): + def __init__(self, message, detail=None): + self.detail = detail + super().__init__(f"{message} Details: {detail}" if detail else message) + + +def _connect_readonly(database_path): + path = Path(database_path).resolve() + return sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True) + + +def validate_storage_database(database_path): + path = Path(database_path) + if not path.exists(): + return + + connection = None + try: + connection = _connect_readonly(path) + connection.execute("PRAGMA schema_version").fetchone() + result = connection.execute("PRAGMA quick_check").fetchone() + if result and result[0] != "ok": + raise GuiStorageDatabaseError( + "The local storage database failed SQLite integrity checks.", + str(result[0]), + ) + table_rows = connection.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall() + table_names = {row[0] for row in table_rows} + known_tables = { + "applications", + "application_versions", + "cached_sync", + "client_versions", + "database_version", + "encryptedproxies", + "locations", + "operators", + "subscription_plans", + } + if table_names and table_names.isdisjoint(known_tables): + raise GuiStorageDatabaseError( + "The local storage database does not look like a HydraVeil storage database.", + ", ".join(sorted(table_names)), + ) + except GuiStorageDatabaseError: + raise + except sqlite3.Error as error: + raise GuiStorageDatabaseError( + "The local storage database could not be read. It may be malformed, stale, or incompatible.", + str(error), + ) from error + finally: + if connection is not None: + connection.close() + + +def _table_has_rows(database_path, table_name): + path = Path(database_path) + if not path.exists(): + return False + + connection = None + try: + connection = _connect_readonly(path) + return connection.execute(f'SELECT 1 FROM "{table_name}" LIMIT 1').fetchone() is not None + except sqlite3.OperationalError as error: + if "no such table" in str(error).lower(): + return False + raise GuiStorageDatabaseError( + "The local storage database could not be checked for synced data.", + str(error), + ) from error + except sqlite3.Error as error: + raise GuiStorageDatabaseError( + "The local storage database could not be checked for synced data.", + str(error), + ) from error + finally: + if connection is not None: + connection.close() + + +def has_required_sync_data(database_path): + required_tables = ("applications", "application_versions", "locations") + return all(_table_has_rows(database_path, table_name) for table_name in required_tables) diff --git a/gui/v2/actions/settings_data.py b/gui/v2/actions/settings_data.py index 851743b..aa4e355 100755 --- a/gui/v2/actions/settings_data.py +++ b/gui/v2/actions/settings_data.py @@ -53,16 +53,23 @@ def read_bwrap_enabled(): def prepare(gui_config_file): - profiles = ProfileController.get_all() + try: + profiles = ProfileController.get_all() + except Exception: + profiles = {} profile_order = normalize_profile_order(gui_config_file, profiles.keys()) try: endpoint_verification_enabled = ConfigurationController.get_endpoint_verification_enabled() except Exception: endpoint_verification_enabled = False + try: + current_connection = ConfigurationController.get_connection() + except Exception: + current_connection = None return { "profiles": profiles, "profile_order": profile_order, - "current_connection": ConfigurationController.get_connection(), + "current_connection": current_connection, "endpoint_verification_enabled": endpoint_verification_enabled, "systemwide_enabled": read_systemwide_enabled(), "bwrap_enabled": read_bwrap_enabled(), @@ -115,8 +122,9 @@ def truncate_key(text, max_length=50): def build_verification_view(profile): operator = None - if profile and getattr(profile, 'location', None) and profile.location.operator: - operator = profile.location.operator + location = getattr(profile, 'location', None) if profile else None + if location and not isinstance(location, dict) and getattr(location, 'operator', None): + operator = location.operator if not operator: return { "operator_name": "N/A", diff --git a/gui/v2/infrastructure/connection_manager.py b/gui/v2/infrastructure/connection_manager.py index b5aa5f8..484f41d 100755 --- a/gui/v2/infrastructure/connection_manager.py +++ b/gui/v2/infrastructure/connection_manager.py @@ -19,7 +19,10 @@ class ConnectionManager: self.profile_button_objects[profile_id] = profile_button_objects def get_available_resolutions(self, profile_id): - profile = ProfileController.get(profile_id) + try: + profile = ProfileController.get(profile_id) + except Exception: + profile = None if profile and hasattr(profile, 'resolution') and profile.resolution: self.available_resolutions.append(profile.resolution) return self.available_resolutions diff --git a/gui/v2/ui/pages/browser_page.py b/gui/v2/ui/pages/browser_page.py index 4834467..99ec63d 100755 --- a/gui/v2/ui/pages/browser_page.py +++ b/gui/v2/ui/pages/browser_page.py @@ -21,10 +21,10 @@ class BrowserPage(Page): self.title.setText("Pick a Browser") self.button_back.setVisible(True) - cm = main_window.connection_manager - if cm.is_synced(): + self.connection_manager = main_window.connection_manager + if self.connection_manager.is_synced(): from gui.v2.actions.sync import generate_grid_positions - browsers = cm.get_browser_list() + browsers = self.connection_manager.get_browser_list() positions = generate_grid_positions(len(browsers)) available = [(QPushButton, brw, positions[i]) for i, brw in enumerate(browsers)] self.create_interface_elements(available) @@ -239,3 +239,21 @@ class BrowserPage(Page): def gestionar_next(self): self.custom_window.navigator.navigate("screen") + + def gestionar_back(self): + profile_data = self.update_status.read_data() + protocol = profile_data.get("protocol", "") + + if not self.connection_manager.is_synced() or not self.connection_manager.get_browser_list(): + self.custom_window.navigator.navigate("menu") + return + + if protocol == "wireguard": + self.custom_window.navigator.navigate("location") + return + + if protocol in ("hidetor", "residential"): + self.custom_window.navigator.navigate("hidetor") + return + + self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/ui/pages/editor_page.py b/gui/v2/ui/pages/editor_page.py index c5bc420..9d2fc53 100755 --- a/gui/v2/ui/pages/editor_page.py +++ b/gui/v2/ui/pages/editor_page.py @@ -359,14 +359,25 @@ class EditorPage(Page): if browser_version == '': browser_version = data_profile.get('browser_version', '') browser_value = f"{browser_type} {browser_version}" - if connection == 'system-wide' or not browser_value.strip(): + fallback_path = os.path.join( + self.btn_path, "default_browser_button.png") + normalized_browser = browser_value.strip().lower() + unknown_browser = ( + not normalized_browser + or normalized_browser == "unknown" + or normalized_browser.startswith("unknown browser") + ) + if connection == 'system-wide': base_image = QPixmap() + elif unknown_browser: + base_image = QPixmap(fallback_path) + if base_image.isNull(): + base_image = BrowserPage.create_browser_button_image( + "Browser", fallback_path, True) else: base_image = BrowserPage.create_browser_button_image( browser_value, self.btn_path) if base_image.isNull(): - fallback_path = os.path.join( - self.btn_path, "default_browser_button.png") base_image = BrowserPage.create_browser_button_image( browser_value, fallback_path, True) elif key == 'location': @@ -593,8 +604,14 @@ class EditorPage(Page): self.on_sync_complete_for_edit_profile) return - previous_index = (index - 1) % len(parameters[key]) - previous_value = parameters[key][previous_index] + values = parameters.get(key, []) + if not values: + self.update_status.update_status( + f"No {key} data available. Sync the database and try again.") + return + + previous_index = (index - 1) % len(values) + previous_value = values[previous_index] self.update_temp_value(key, previous_value) def show_next_value(self, key: str, index: int, parameters: dict) -> None: @@ -606,8 +623,14 @@ class EditorPage(Page): self.on_sync_complete_for_edit_profile) return - next_index = (index + 1) % len(parameters[key]) - next_value = parameters[key][next_index] + values = parameters.get(key, []) + if not values: + self.update_status.update_status( + f"No {key} data available. Sync the database and try again.") + return + + next_index = (index + 1) % len(values) + next_value = values[next_index] self.update_temp_value(key, next_value) diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index 62009aa..16ae7ad 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -12,6 +12,7 @@ from PyQt6.QtWidgets import ( from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ProfileController import ProfileController +from core.errors.logger import logger from core.controllers.tickets.UseTicketController import ( do_we_use_a_random_ticket, get_unused_tickets, @@ -20,6 +21,7 @@ from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from gui.v2.infrastructure.setup_observers import ticket_observer +from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.actions.profile_order import normalize_profile_order from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.location_verification_page import LocationVerificationPage @@ -205,6 +207,88 @@ class MenuPage(Page): except RuntimeError: pass + def _safe_profiles(self): + try: + return ProfileController.get_all() + except GuiStorageDatabaseError as error: + logger.error(f"[MENU] Storage database error while loading profiles: {error}") + self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.") + except Exception as error: + logger.error(f"[MENU] Error while loading profiles: {error}") + self.update_status.update_status("Could not load profiles. Sync or restart and try again.") + return {} + + def _safe_profile(self, profile_id): + try: + return ProfileController.get(int(profile_id)) + except GuiStorageDatabaseError as error: + logger.error(f"[MENU] Storage database error while loading profile {profile_id}: {error}") + self.update_status.update_status("Local storage database could not be read. Restart and recover storage.db.") + except Exception as error: + logger.error(f"[MENU] Error while loading profile {profile_id}: {error}") + self.update_status.update_status("Could not load profile data. Sync or restart and try again.") + return None + + def _connection_code(self, profile): + connection = getattr(profile, 'connection', None) + return getattr(connection, 'code', None) or 'unknown' + + def _location_key(self, profile): + location = getattr(profile, 'location', None) + if isinstance(location, dict): + country_code = location.get("country_code") or "na" + code = location.get("code") or "na" + return f'{country_code}_{code}' + + country_code = getattr(location, 'country_code', None) + code = getattr(location, 'code', None) + if country_code and code: + return f'{country_code}_{code}' + + return 'na_na' + + def _browser_info(self, profile): + if not isinstance(profile, SessionProfile): + return "unknown browser", "", False + + application_version = getattr(profile, 'application_version', None) + if isinstance(application_version, dict): + browser = application_version.get('application_code') or "unknown browser" + version = application_version.get('version_number') or "" + return browser, version, False + + browser = getattr(application_version, 'application_code', None) or "unknown browser" + version = getattr(application_version, 'version_number', None) or "" + supported = bool(getattr(application_version, 'supported', False)) + return browser, version, supported + + def _profile_incomplete_reason(self, profile): + if profile is None: + return "Profile data could not be loaded. Sync or restart and try again." + + connection = getattr(profile, 'connection', None) + if not getattr(connection, 'code', None): + return "Profile connection data is incomplete. Sync the database and try again." + + location = getattr(profile, 'location', None) + if location is None or isinstance(location, dict): + return "Profile location data is incomplete. Sync the database and try again." + + if isinstance(profile, SessionProfile): + application_version = getattr(profile, 'application_version', None) + if isinstance(application_version, dict) or application_version is None: + return "Profile browser data is incomplete. Sync the database and try again." + if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None): + return "Profile browser data is incomplete. Sync the database and try again." + + return None + + def _location_operator(self, profile): + location = getattr(profile, 'location', None) + if isinstance(location, dict) or location is None: + return None + return getattr(location, 'operator', None) + def match_core_profiles(self, profiles_dict): new_dict = {} @@ -213,26 +297,13 @@ class MenuPage(Page): profiles_dict.keys()) for idx in profile_ids: - try: - profile = profiles_dict[idx] - new_profile = {} + profile = profiles_dict.get(idx) + if profile is None: + continue - protocol = profile.connection.code - - # print(f"DEBUG: the type is = {type(profile.location)}, value = {profile.location}") - - if isinstance(profile.location, dict): - # Fake/missing data — show placeholders from data, - country_code = profile.location.get("country_code", "na") - code = profile.location.get("code", "na") - location = f'{country_code}_{code}' - else: - # Proper Location object - location = f'{profile.location.country_code}_{profile.location.code}' - except: - import sys - print(f"idx: {idx}") - sys.exit("STOPPING TO READ PRINTS FOR IDX. this failed on getting the location data inside match_core_profiles") + new_profile = {} + protocol = self._connection_code(profile) + location = self._location_key(profile) new_profile['location'] = location @@ -251,23 +322,17 @@ class MenuPage(Page): else: new_profile['connection'] = 'just proxy' - if isinstance(profile, SessionProfile): - browser = profile.application_version.application_code - else: - browser = 'unknown' - if browser != 'unknown': - new_profile['browser'] = browser - new_profile['browser_version'] = profile.application_version.version_number - new_profile['browser_supported'] = profile.application_version.supported - else: - new_profile['browser'] = 'unknown browser' - new_profile['browser_supported'] = False + browser, browser_version, browser_supported = self._browser_info(profile) + new_profile['browser'] = browser + new_profile['browser_version'] = browser_version + new_profile['browser_supported'] = browser_supported resolution = profile.resolution if hasattr( profile, 'resolution') else 'None' new_profile['dimentions'] = resolution - new_profile['name'] = profile.name + new_profile['name'] = getattr(profile, 'name', None) or f'Profile {idx}' + new_profile['incomplete_reason'] = self._profile_incomplete_reason(profile) new_dict[f'Profile_{idx}'] = new_profile @@ -281,7 +346,7 @@ class MenuPage(Page): if hasattr(self, 'verification_button'): self.verification_button.setEnabled(False) self.profiles_data = self.match_core_profiles( - ProfileController.get_all()) + self._safe_profiles()) self.number_of_profiles = len(self.profiles_data) self.profile_info = dict(self.profiles_data) @@ -296,7 +361,7 @@ class MenuPage(Page): def refresh_profiles_data(self): self.profiles_data = self.match_core_profiles( - ProfileController.get_all()) + self._safe_profiles()) self.number_of_profiles = len(self.profiles_data) self.profile_info = dict(self.profiles_data) for profile_name, profile_value in self.profiles_data.items(): @@ -304,12 +369,16 @@ class MenuPage(Page): self.update_scroll_widget_size() def refresh_menu_buttons(self): - profiles = ProfileController.get_all() + profiles = self._safe_profiles() self.button_states.clear() self.connection_manager._connected_profiles.clear() self.IsSystem = 0 for profile_id, profile in profiles.items(): - if ProfileController.is_enabled(profile): + try: + is_enabled = ProfileController.is_enabled(profile) + except Exception: + is_enabled = False + if is_enabled: self.connection_manager.add_connected_profile(profile_id) if isinstance(profile, SessionProfile): if profile.connection and profile.connection.code == 'tor': @@ -424,7 +493,7 @@ class MenuPage(Page): def create_profile_name_label(self, parent_label, name): child_label = QLabel(parent_label) child_label.setGeometry(0, 65, 175, 30) - child_label.setText(name) + child_label.setText(str(name or "Unnamed Profile")) child_label.setAlignment(Qt.AlignmentFlag.AlignCenter) child_label.setAttribute( Qt.WidgetAttribute.WA_TransparentForMouseEvents) @@ -537,9 +606,7 @@ class MenuPage(Page): verification_layout.setContentsMargins(20, 20, 20, 20) verification_layout.setSpacing(15) - operator = None - if profile_obj.location and profile_obj.location.operator: - operator = profile_obj.location.operator + operator = self._location_operator(profile_obj) info_items = [ ("Operator Name", "operator_name"), @@ -696,14 +763,18 @@ class MenuPage(Page): connection = profile.get("connection", "") country_garaje = profile.get("country_garaje", "") - profile_obj = ProfileController.get(self.reverse_id) + profile_obj = self._safe_profile(self.reverse_id) is_profile_enabled = self.connection_manager.is_profile_connected( self.reverse_id) show_verification_widget = False label_principal = None label_tor = None text_color = "white" - operator_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else "" + profile_location = getattr(profile_obj, 'location', None) + if isinstance(profile_location, dict): + profile_location = None + profile_operator = self._location_operator(profile_obj) + operator_name = getattr(profile_operator, 'name', '') if profile_operator else "" if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard': verification_widget = self.create_verification_widget( @@ -713,7 +784,7 @@ class MenuPage(Page): label_principal = None elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard": - if profile_obj.is_session_profile(): + if isinstance(profile_obj, SessionProfile): text_color = "black" label_background = QLabel(self) label_background.setGeometry(0, 60, 410, 354) @@ -733,9 +804,9 @@ class MenuPage(Page): l_img.setScaledContents(True) l_img.show() self.additional_labels.append(l_img) - l_name = f"{profile_obj.location.country_name}, {profile_obj.location.name}" if profile_obj.location else "" - o_name = profile_obj.location.operator.name if profile_obj.location and profile_obj.location.operator else "" - n_key = profile_obj.location.operator.nostr_public_key if profile_obj.location and profile_obj.location.operator else "" + l_name = f"{profile_location.country_name}, {profile_location.name}" if profile_location else "" + o_name = getattr(profile_operator, 'name', '') if profile_operator else "" + n_key = getattr(profile_operator, 'nostr_public_key', '') if profile_operator else "" info_txt = QTextEdit(self) info_txt.setGeometry(130, 110, 260, 250) @@ -884,11 +955,12 @@ class MenuPage(Page): self.current_profile_location = location def show_profile_verification(self, profile_id): - profile = ProfileController.get(profile_id) - if not profile or not profile.location: + profile = self._safe_profile(profile_id) + profile_location = getattr(profile, 'location', None) + if not profile or not profile_location or isinstance(profile_location, dict): return - location_key = f'{profile.location.country_code}_{profile.location.code}' + location_key = f'{profile_location.country_code}_{profile_location.code}' location_info = self.connection_manager.get_location_info(location_key) if not location_info: return @@ -919,9 +991,22 @@ class MenuPage(Page): self.boton_just.setEnabled(False) self.boton_just_session.setEnabled(False) - profile = ProfileController.get(int(self.reverse_id)) - is_tor = self.update_status.get_current_connection() == 'tor' - if profile.connection.code == 'tor' and not profile.application_version.installed and not is_tor: + profile = self._safe_profile(int(self.reverse_id)) + incomplete_reason = self._profile_incomplete_reason(profile) + if incomplete_reason: + self.update_status.update_status(incomplete_reason) + self.boton_just.setEnabled(True) + self.boton_just_session.setEnabled(True) + return + + try: + is_tor = self.update_status.get_current_connection() == 'tor' + except Exception: + is_tor = False + + connection = getattr(profile, 'connection', None) + application_version = getattr(profile, 'application_version', None) + if getattr(connection, 'code', None) == 'tor' and not getattr(application_version, 'installed', False) and not is_tor: message = f'You are using a Tor profile, but the associated browser is not installed. If you want the browser to be downloaded with Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.' else: @@ -973,7 +1058,12 @@ class MenuPage(Page): profile_data = { 'id': int(self.reverse_id) } - profile = ProfileController.get(int(self.reverse_id)) + profile = self._safe_profile(int(self.reverse_id)) + if profile is None: + self.update_status.update_status("Could not load profile data. Sync or restart and try again.") + self.disconnect_button.setEnabled(True) + self.disconnect_system_wide_button.setEnabled(True) + return is_session_profile = isinstance(profile, SessionProfile) if not is_session_profile: connected_profiles = self.connection_manager.get_connected_profiles() @@ -1030,7 +1120,7 @@ class MenuPage(Page): 'Sync failed. Please try again later.') def change_connect_button(self): - profile = ProfileController.get(int(self.reverse_id)) + profile = self._safe_profile(int(self.reverse_id)) is_connected = self.connection_manager.is_profile_connected( int(self.reverse_id)) is_session_profile = isinstance(profile, SessionProfile) @@ -1047,6 +1137,10 @@ class MenuPage(Page): is_connected and not is_session_profile) def update_status_message(self, profile, is_connected): + if profile is None: + self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.") + return + if is_connected: message = Worker.generate_profile_message(profile, is_enabled=True) self.update_status.enable_marquee(message) @@ -1214,9 +1308,15 @@ class MenuPage(Page): def change_app_page(self, text, Is_changed): self.update_status.update_status(str(text)) if Is_changed: - current_connection = self.update_status.get_current_connection() - profile = ProfileController.get(int(self.reverse_id)) - if current_connection != 'tor' and profile.connection.code == 'tor': + try: + current_connection = self.update_status.get_current_connection() + except Exception: + current_connection = None + profile = self._safe_profile(int(self.reverse_id)) + incomplete_reason = self._profile_incomplete_reason(profile) + if incomplete_reason: + self.update_status.update_status(incomplete_reason) + elif current_connection != 'tor' and getattr(getattr(profile, 'connection', None), 'code', None) == 'tor': message = f'You are using a Tor profile, but the profile subscription is missing or expired. If you want the billing to be done thourgh Tor, you must switch to a Tor connection. Otherwise, proceed with a clearweb connection.' self.popup = self._make_confirmation_popup(message, button_text='Proceed') self.popup.finished.connect( @@ -1261,7 +1361,7 @@ class MenuPage(Page): self.boton_edit.setEnabled(False) if profile_id == self.reverse_id: - profile_obj = ProfileController.get(profile_id) + profile_obj = self._safe_profile(profile_id) if profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard' and ConfigurationController.get_endpoint_verification_enabled(): self.print_profile_details(f"Profile_{profile_id}") else: diff --git a/gui/v2/ui/popups/generic_choice.py b/gui/v2/ui/popups/generic_choice.py old mode 100644 new mode 100755 diff --git a/gui/v2/ui/popups/terminal_threading.py b/gui/v2/ui/popups/terminal_threading.py old mode 100644 new mode 100755 diff --git a/gui/v2/workers/worker.py b/gui/v2/workers/worker.py index d729783..7ded10e 100755 --- a/gui/v2/workers/worker.py +++ b/gui/v2/workers/worker.py @@ -25,6 +25,7 @@ from core.Errors import ( ) from gui.v2.actions.locations import location_candidates +from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.infrastructure.setup_observers import ( application_version_observer, connection_observer, @@ -50,7 +51,22 @@ class Worker(QObject): self._consumed_ticket = None def run(self): - self.profile = ProfileController.get(int(self.profile_data['id'])) + try: + self.profile = ProfileController.get(int(self.profile_data['id'])) + except GuiStorageDatabaseError: + self.update_signal.emit( + "Local storage database could not be read. Restart and recover storage.db.", False, None, None, None) + return + except Exception: + self.update_signal.emit( + "Could not load profile data. Sync or restart and try again.", False, None, None, None) + return + + incomplete_reason = self._profile_incomplete_reason() + if incomplete_reason: + self.update_signal.emit(incomplete_reason, False, None, None, None) + return + if 'use_ticket' in self.profile_data: ticket_billing_code = self._consume_ticket( self.profile_data['use_ticket'], @@ -131,6 +147,27 @@ class Worker(QObject): self.update_signal.emit( f"No profile found with ID: {self.profile_data['id']}", False, None, None, None) + def _profile_incomplete_reason(self): + if self.profile is None: + return None + + connection = getattr(self.profile, 'connection', None) + if not getattr(connection, 'code', None): + return "Profile connection data is incomplete. Sync the database and try again." + + location = getattr(self.profile, 'location', None) + if location is None or isinstance(location, dict): + return "Profile location data is incomplete. Sync the database and try again." + + if isinstance(self.profile, SessionProfile): + application_version = getattr(self.profile, 'application_version', None) + if isinstance(application_version, dict) or application_version is None: + return "Profile browser data is incomplete. Sync the database and try again." + if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None): + return "Profile browser data is incomplete. Sync the database and try again." + + return None + def _location_candidates(self, preferred=None): return location_candidates(self.profile, preferred) diff --git a/gui/v2/workers/worker_thread.py b/gui/v2/workers/worker_thread.py index ea78ef3..3fc8502 100755 --- a/gui/v2/workers/worker_thread.py +++ b/gui/v2/workers/worker_thread.py @@ -168,12 +168,19 @@ class WorkerThread(QThread): self.finished.emit(False) def list_profiles(self): - profiles = ProfileController.get_all() + try: + profiles = ProfileController.get_all() + except Exception: + profiles = {} + self.text_output.emit("Could not load profiles. Sync or restart and try again.") self.profiles_output.emit(profiles) def create_profile(self, profile_type): - location = LocationController.get( - self.profile_data['country_code'], self.profile_data['code']) + try: + location = LocationController.get( + self.profile_data['country_code'], self.profile_data['code']) + except Exception: + location = None if location is None: self.text_output.emit( f"Invalid location code: {self.profile_data['location_code']}") @@ -188,8 +195,11 @@ class WorkerThread(QThread): application_details = self.profile_data['application'].split( ':', 1) - application_version = ApplicationVersionController.get( - application_details[0], application_details[1] if len(application_details) > 1 else None) + try: + application_version = ApplicationVersionController.get( + application_details[0], application_details[1] if len(application_details) > 1 else None) + except Exception: + application_version = None if application_version is None: self.text_output.emit( f"Invalid application: {self.profile_data['application']}")