Skip to content

Commit

Permalink
Improve bad connections handling (#3075)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dartvader316 authored Sep 21, 2023
1 parent b6eb91a commit 9874515
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 10 deletions.
18 changes: 13 additions & 5 deletions bottles/backend/managers/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,21 @@ 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()}

# 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):
Expand All @@ -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()
Expand All @@ -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:
Expand Down
25 changes: 23 additions & 2 deletions bottles/backend/managers/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -127,3 +146,5 @@ def query(_repo, _data):

for t in threads:
t.join()

self.do_get_index = True
1 change: 1 addition & 0 deletions bottles/backend/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions bottles/backend/utils/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
12 changes: 12 additions & 0 deletions bottles/frontend/ui/loading.blp
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions bottles/frontend/ui/preferences.blp
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
10 changes: 9 additions & 1 deletion bottles/frontend/views/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -31,12 +32,19 @@ 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):
total: int = res.data
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))
2 changes: 2 additions & 0 deletions bottles/frontend/views/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions bottles/frontend/windows/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions data/com.usebottles.bottles.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
<summary>Dark theme</summary>
<description>Force the use of dark theme.</description>
</key>
<key type="b" name="force-offline">
<default>false</default>
<summary>Force Offline</summary>
<description>"Force disable any network activity even with available network connection."</description>
</key>
<key type="b" name="update-date">
<default>false</default>
<summary>Toggle update date in list</summary>
Expand Down

0 comments on commit 9874515

Please sign in to comment.