From 9874515775e8c3945d84752e29dcf535a441b9ae Mon Sep 17 00:00:00 2001 From: Kirill <20815458+dartvader316@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:26:46 +0000 Subject: [PATCH] Improve bad connections handling (#3075) * backend: Add ForceStopNetworking signal * backend: Add stop functionality for connection * backend: Add stop functionality for repository getting index * backend: add option to skip connection check for manager * frontend: skip connection check in manager if aborted in MainWindow connection * frontend: add button to skip connections at startup loading * data: add force-offline option in gschema * frontend: add force-offline switch in preferences * backend: use force-offline setting in ConnectionUtils creation for manager * frontend: use force-offline setting in ConnectionUtils creation for MainWindow * fix: loading btn_go_offline label and bottom margin --- bottles/backend/managers/manager.py | 18 +++++++++++++----- bottles/backend/managers/repository.py | 25 +++++++++++++++++++++++-- bottles/backend/state.py | 1 + bottles/backend/utils/connection.py | 17 +++++++++++++++++ bottles/frontend/ui/loading.blp | 12 ++++++++++++ bottles/frontend/ui/preferences.blp | 10 ++++++++++ bottles/frontend/views/loading.py | 10 +++++++++- bottles/frontend/views/preferences.py | 2 ++ bottles/frontend/windows/main_window.py | 6 ++++-- data/com.usebottles.bottles.gschema.xml | 5 +++++ 10 files changed, 96 insertions(+), 10 deletions(-) diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index a7f6a9c5ce..97e0174c50 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -103,7 +103,7 @@ class Manager(metaclass=Singleton): supported_dependencies = {} supported_installers = {} - def __init__(self, g_settings: Any = None, is_cli: bool = False, **kwargs): + def __init__(self, g_settings: Any = None, check_connection: bool = True, is_cli: bool = False, **kwargs): super().__init__(**kwargs) times = {"start": time.time()} @@ -111,10 +111,13 @@ def __init__(self, g_settings: Any = None, is_cli: bool = False, **kwargs): # common variables self.is_cli = is_cli self.settings = g_settings or GSettingsStub - self.utils_conn = ConnectionUtils(force_offline=self.is_cli) + self.utils_conn = ConnectionUtils(force_offline=self.is_cli or self.settings.get_boolean("force-offline")) self.data_mgr = DataManager() - _offline = not self.utils_conn.check_connection() - + _offline = True + + if check_connection: + _offline = not self.utils_conn.check_connection() + # validating user-defined Paths.bottles if user_bottles_path := self.data_mgr.get(UserDataKeys.CustomBottlesPath): if os.path.exists(user_bottles_path): @@ -126,7 +129,11 @@ def __init__(self, g_settings: Any = None, is_cli: bool = False, **kwargs): ) # sub-managers - self.repository_manager = RepositoryManager() + self.repository_manager = RepositoryManager(get_index= not _offline) + if self.repository_manager.aborted_connections > 0: + self.utils_conn.status = False + _offline = True + times["RepositoryManager"] = time.time() self.versioning_manager = VersioningManager(self) times["VersioningManager"] = time.time() @@ -138,6 +145,7 @@ def __init__(self, g_settings: Any = None, is_cli: bool = False, **kwargs): self.steam_manager = SteamManager() times["SteamManager"] = time.time() + if not self.is_cli: times.update(self.checks(install_latest=False, first_run=True).data) else: diff --git a/bottles/backend/managers/repository.py b/bottles/backend/managers/repository.py index 6ef03e89c3..30a6b4e4a3 100644 --- a/bottles/backend/managers/repository.py +++ b/bottles/backend/managers/repository.py @@ -50,9 +50,14 @@ class RepositoryManager: } } - def __init__(self): + def __init__(self, get_index=True): + self.do_get_index = True + self.aborted_connections = 0 + SignalManager.connect(Signals.ForceStopNetworking, self.__stop_index) + self.__check_locals() - self.__get_index() + if get_index: + self.__get_index() def get_repo(self, name: str, offline: bool = False): if name in self.__repositories: @@ -88,6 +93,18 @@ def __check_locals(self): else: logging.error(f"Local {repo} path does not exist: {_path}") + + def __curl_progress(self, _download_t, _download_d, _upload_t, _upload_d): + if self.do_get_index: + return pycurl.E_OK + else: + self.aborted_connections+=1 + return pycurl.E_ABORTED_BY_CALLBACK + + def __stop_index(self, res: Result): + if res.status: + self.do_get_index = False + def __get_index(self): total = len(self.__repositories) @@ -104,6 +121,8 @@ def query(_repo, _data): c.setopt(c.NOBODY, True) c.setopt(c.FOLLOWLOCATION, True) c.setopt(c.TIMEOUT, 10) + c.setopt(c.NOPROGRESS, False) + c.setopt(c.XFERINFOFUNCTION, self.__curl_progress) try: c.perform() @@ -127,3 +146,5 @@ def query(_repo, _data): for t in threads: t.join() + + self.do_get_index = True diff --git a/bottles/backend/state.py b/bottles/backend/state.py index 1c976be38b..2e22e4bd7d 100644 --- a/bottles/backend/state.py +++ b/bottles/backend/state.py @@ -28,6 +28,7 @@ class Signals(Enum): """Signals backend support""" ManagerLocalBottlesLoaded = "Manager.local_bottles_loaded" # no extra data + ForceStopNetworking = "LoadingView.stop_networking" # status(bool): Force Stop network operations RepositoryFetched = "RepositoryManager.repo_fetched" # status: fetch success or not, data(int): total repositories NetworkStatusChanged = "ConnectionUtils.status_changed" # status(bool): network ready or not diff --git a/bottles/backend/utils/connection.py b/bottles/backend/utils/connection.py index 3a0e15b107..d1c4a3c209 100644 --- a/bottles/backend/utils/connection.py +++ b/bottles/backend/utils/connection.py @@ -41,6 +41,9 @@ class ConnectionUtils: def __init__(self, force_offline=False, **kwargs): super().__init__(**kwargs) self.force_offline = force_offline + self.do_check_connection = True + self.aborted_connections = 0 + SignalManager.connect(Signals.ForceStopNetworking, self.stop_check) @property def status(self) -> Optional[bool]: @@ -54,6 +57,17 @@ def status(self, value: bool): self._status = value SignalManager.send(Signals.NetworkStatusChanged, Result(status=self.status)) + def __curl_progress(self, _download_t, _download_d, _upload_t, _upload_d): + if self.do_check_connection: + return pycurl.E_OK + else: + self.aborted_connections+=1 + return pycurl.E_ABORTED_BY_CALLBACK + + def stop_check(self, res: Result): + if res.status: + self.do_check_connection = False + def check_connection(self, show_notification=False) -> bool: """check network status, send result through signal NetworkReady and return""" if self.force_offline or "FORCE_OFFLINE" in os.environ: @@ -66,6 +80,8 @@ def check_connection(self, show_notification=False) -> bool: c.setopt(c.URL, 'https://ping.usebottles.com') c.setopt(c.FOLLOWLOCATION, True) c.setopt(c.NOBODY, True) + c.setopt(c.NOPROGRESS, False) + c.setopt(c.XFERINFOFUNCTION, self.__curl_progress) c.perform() if c.getinfo(pycurl.HTTP_CODE) != 200: @@ -84,4 +100,5 @@ def check_connection(self, show_notification=False) -> bool: self.last_check = datetime.now() self.status = False finally: + self.do_check_connection = True return self.status diff --git a/bottles/frontend/ui/loading.blp b/bottles/frontend/ui/loading.blp index efaca118d7..99308467f0 100644 --- a/bottles/frontend/ui/loading.blp +++ b/bottles/frontend/ui/loading.blp @@ -15,6 +15,18 @@ template LoadingView : .AdwBin { vexpand: true; } + Button btn_go_offline { + margin-bottom: 20; + valign: center; + halign: center; + label: _("Continue Offline"); + + styles [ + "destructive-action", + "pill", + ] + } + Label label_fetched { styles [ "dim-label", diff --git a/bottles/frontend/ui/preferences.blp b/bottles/frontend/ui/preferences.blp index 3ec84eaa56..fcad73db5f 100644 --- a/bottles/frontend/ui/preferences.blp +++ b/bottles/frontend/ui/preferences.blp @@ -138,6 +138,16 @@ template PreferencesWindow : Adw.PreferencesWindow { } } + Adw.ActionRow { + title: _("Force Offline Mode"); + subtitle: _("Force disable any network activity even with available network connection."); + activatable-widget: switch_force_offline; + + Switch switch_force_offline { + valign: center; + } + } + Adw.ActionRow { title: _("Bottles Directory"); subtitle: _("Directory that contains the data of your Bottles."); diff --git a/bottles/frontend/views/loading.py b/bottles/frontend/views/loading.py index 9f80f5ac19..44418a66ea 100644 --- a/bottles/frontend/views/loading.py +++ b/bottles/frontend/views/loading.py @@ -20,6 +20,7 @@ from gi.repository import Gtk, Adw from bottles.backend.models.result import Result +from bottles.backend.state import SignalManager, Signals from bottles.frontend.utils.gtk import GtkUtils @@ -31,8 +32,12 @@ class LoadingView(Adw.Bin): # region widgets label_fetched = Gtk.Template.Child() label_downloading = Gtk.Template.Child() - + btn_go_offline = Gtk.Template.Child() # endregion + + def __init__(self,**kwargs): + super().__init__(**kwargs) + self.btn_go_offline.connect("clicked", self.go_offline) @GtkUtils.run_in_main_loop def add_fetched(self, res: Result): @@ -40,3 +45,6 @@ def add_fetched(self, res: Result): self.__fetched += 1 self.label_downloading.set_text(_("Downloading ~{0} of packages…").format("20kb")) self.label_fetched.set_text(_("Fetched {0} of {1} packages").format(self.__fetched, total)) + + def go_offline(self, _widget): + SignalManager.send(Signals.ForceStopNetworking, Result(status=True)) \ No newline at end of file diff --git a/bottles/frontend/views/preferences.py b/bottles/frontend/views/preferences.py index cee476de5c..01142ee43f 100644 --- a/bottles/frontend/views/preferences.py +++ b/bottles/frontend/views/preferences.py @@ -43,6 +43,7 @@ class PreferencesWindow(Adw.PreferencesWindow): row_theme = Gtk.Template.Child() switch_theme = Gtk.Template.Child() switch_notifications = Gtk.Template.Child() + switch_force_offline = Gtk.Template.Child() switch_temp = Gtk.Template.Child() switch_release_candidate = Gtk.Template.Child() switch_steam = Gtk.Template.Child() @@ -91,6 +92,7 @@ def __init__(self, window, **kwargs): # bind widgets self.settings.bind("dark-theme", self.switch_theme, "active", Gio.SettingsBindFlags.DEFAULT) self.settings.bind("notifications", self.switch_notifications, "active", Gio.SettingsBindFlags.DEFAULT) + self.settings.bind("force-offline", self.switch_force_offline, "active", Gio.SettingsBindFlags.DEFAULT) self.settings.bind("temp", self.switch_temp, "active", Gio.SettingsBindFlags.DEFAULT) # Connect RC signal to another func self.settings.bind("release-candidate", self.switch_release_candidate, "active", Gio.SettingsBindFlags.DEFAULT) diff --git a/bottles/frontend/windows/main_window.py b/bottles/frontend/windows/main_window.py index 24481f14b6..4f516bb3b4 100644 --- a/bottles/frontend/windows/main_window.py +++ b/bottles/frontend/windows/main_window.py @@ -81,7 +81,7 @@ def __init__(self, arg_bottle, **kwargs): super().__init__(**kwargs, default_width=width, default_height=height) self.disable_onboard = False - self.utils_conn = ConnectionUtils() + self.utils_conn = ConnectionUtils(force_offline=self.settings.get_boolean("force-offline")) self.manager = None self.arg_bottle = arg_bottle self.app = kwargs.get("application") @@ -226,7 +226,9 @@ def set_manager(result: Manager, error=None): def get_manager(): if self.utils_conn.check_connection(): SignalManager.connect(Signals.RepositoryFetched, self.page_loading.add_fetched) - mng = Manager(g_settings=self.settings) + + # do not redo connection if aborted connection + mng = Manager(g_settings=self.settings, check_connection=self.utils_conn.aborted_connections == 0) return mng self.check_core_deps() diff --git a/data/com.usebottles.bottles.gschema.xml b/data/com.usebottles.bottles.gschema.xml index 2164f51290..f8167f3086 100644 --- a/data/com.usebottles.bottles.gschema.xml +++ b/data/com.usebottles.bottles.gschema.xml @@ -11,6 +11,11 @@ Dark theme Force the use of dark theme. + + false + Force Offline + "Force disable any network activity even with available network connection." + false Toggle update date in list