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