Skip to content
5 changes: 3 additions & 2 deletions bottles/backend/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def download(self) -> Result:
headers=headers,
timeout=(10, 30),
)
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0))
received_size = 0

Expand Down Expand Up @@ -103,10 +104,10 @@ def download(self) -> Result:
"Your system may have a wrong date/time or wrong certificates."
)
return Result(False, message="Download failed due to a SSL error.")
except (requests.exceptions.RequestException, OSError):
except (requests.exceptions.RequestException, OSError) as error:
with suppress(OSError):
os.remove(self.file)
logging.error("Download failed! Check your internet connection.")
logging.error(f"Failed to download [{self.url}]: {error}")
return Result(
False, message="Download failed! Check your internet connection."
)
Expand Down
68 changes: 12 additions & 56 deletions bottles/backend/managers/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
from threading import Event
from typing import Optional

import pycurl

from bottles.backend.downloader import Downloader
from bottles.backend.globals import Paths
from bottles.backend.logger import Logger
Expand Down Expand Up @@ -324,67 +322,25 @@ def download(
return Result(False, message="File is not available in offline mode.")

if not os.path.isfile(file_path):
"""
As some urls can be redirect, we need to take care of this
and make sure to use the final url. This check should be
skipped for large files (e.g. runners).
"""
c = pycurl.Curl()
_proxy = os.environ.get("http_proxy") or os.environ.get("https_proxy")
if _proxy:
c.setopt(pycurl.PROXY, _proxy)
try:
c.setopt(c.URL, download_url) # type: ignore
c.setopt(c.FOLLOWLOCATION, True) # type: ignore
c.setopt(c.HTTPHEADER, ["User-Agent: curl/7.79.1"]) # type: ignore
c.setopt(c.NOBODY, True) # type: ignore
c.setopt(pycurl.CONNECTTIMEOUT, 10)
c.setopt(pycurl.TIMEOUT, 30)
c.perform()

req_code = c.getinfo(c.RESPONSE_CODE) # type: ignore
download_url = c.getinfo(c.EFFECTIVE_URL) # type: ignore
except pycurl.error:
logging.exception(f"Failed to download [{download_url}]")
res = Downloader(
url=download_url,
file=temp_dest,
update_func=update_func,
cancel_event=cancel_event,
).download()

if not res.ok:
if not external_task:
TaskManager.remove(task_id)
return Result(False)
finally:
c.close()
return res

if req_code == 200:
"""
If the status code is 200, the resource should be available
and the download should be started. Any exceptions return
False and the download is removed from the download manager.
"""
res = Downloader(
url=download_url,
file=temp_dest,
update_func=update_func,
cancel_event=cancel_event,
).download()

if not res.ok:
if not external_task:
TaskManager.remove(task_id)
return res

if not os.path.isfile(temp_dest):
"""Fail if the file is not available in the /temp directory."""
if not external_task:
TaskManager.remove(task_id)
return Result(False)

just_downloaded = True
else:
logging.warning(
f"Failed to download [{download_url}] with code: {req_code} != 200"
)
if not os.path.isfile(temp_dest):
if not external_task:
TaskManager.remove(task_id)
return Result(False)

just_downloaded = True

file_path = os.path.join(Paths.temp, existing_file)
if rename and just_downloaded:
"""Renaming the downloaded file if requested."""
Expand Down
59 changes: 43 additions & 16 deletions bottles/backend/managers/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import filecmp
import os
import shutil
import threading
Expand Down Expand Up @@ -163,6 +164,11 @@ def import_thumbnail(source_path, config: Optional[BottleConfig] = None):

try:
grids_path.mkdir(parents=True, exist_ok=True)
for candidate in grids_path.iterdir():
if candidate.is_file() and filecmp.cmp(
source_path, candidate, shallow=False
):
return f"{uri_prefix}{candidate.name}"
shutil.copy2(source_path, destination)
except OSError as error:
logging.warning(f"Could not import library thumbnail: {error}")
Expand Down Expand Up @@ -200,23 +206,11 @@ def set_thumbnail(
managed_prefix = "umu-grid:" if config is None else "grid:"
if (
old_thumbnail
and old_thumbnail != thumbnail
and old_thumbnail.startswith(managed_prefix)
and not thumbnail_is_shared
):
old_filename = old_thumbnail.removeprefix(managed_prefix)
if os.path.basename(old_filename) == old_filename:
if config is None:
old_path = Path(Paths.base) / "umu" / "covers" / old_filename
else:
old_path = (
Path(ManagerUtils.get_bottle_path(config))
/ "grids"
/ old_filename
)
try:
os.remove(old_path)
except FileNotFoundError:
pass
self.__remove_thumbnail(old_thumbnail, config)

return True

Expand Down Expand Up @@ -260,16 +254,49 @@ def __already_in_library(self, data: dict):

return False

def remove_from_library(self, _uuid: str):
@staticmethod
def __remove_thumbnail(
thumbnail: str, config: Optional[BottleConfig] = None
) -> None:
managed_prefix = "umu-grid:" if config is None else "grid:"
if not thumbnail.startswith(managed_prefix):
return

filename = thumbnail.removeprefix(managed_prefix)
if os.path.basename(filename) != filename:
return
if config is None:
path = Path(Paths.base) / "umu" / "covers" / filename
else:
path = Path(ManagerUtils.get_bottle_path(config)) / "grids" / filename
try:
os.remove(path)
except FileNotFoundError:
pass

def remove_from_library(
self, _uuid: str, config: Optional[BottleConfig] = None
):
"""
Removes an entry from the library.yml file.
"""
with self.__lock:
self.load_library(silent=True)
if self.__library.get(_uuid):
entry = self.__library.get(_uuid)
if entry:
logging.info(f"Removing entry from library: {_uuid}")
thumbnail = entry.get("thumbnail")
thumbnail_is_shared = any(
uuid != _uuid and item.get("thumbnail") == thumbnail
for uuid, item in self.__library.items()
)
del self.__library[_uuid]
self.save_library()
if thumbnail and not thumbnail_is_shared:
if entry.get("source") == "umu":
self.__remove_thumbnail(thumbnail)
elif config is not None:
self.__remove_thumbnail(thumbnail, config)
return
logging.warning(f"Entry not found in library, nothing to remove: {_uuid}")

Expand Down
14 changes: 8 additions & 6 deletions bottles/backend/managers/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,6 @@ def get_managed_wine_runners(self) -> list[str]:
runner
for runner in self.runners_available
if not runner.startswith("sys-")
and not SteamUtils.is_proton(ManagerUtils.get_runner_path(runner))
]

def check_runtimes(self, install_latest: bool = True) -> bool:
Expand Down Expand Up @@ -2699,12 +2698,15 @@ def __persist_d7vk_config(
if not saved.ok:
return Result(False, message=saved.message)

config.D7VK = candidate.D7VK
config.Parameters.d7vk = candidate.Parameters.d7vk
config.Update_Date = candidate.Update_Date
if candidate.Name in self.local_bottles:
self.local_bottles[candidate.Name] = candidate
if candidate.Environment == "Steam":
self.steam_manager.update_bottle(candidate)
RegistryRuleManager.apply_rules(candidate, trigger="components")
return Result(True, data={"config": candidate})
self.local_bottles[candidate.Name] = config
if config.Environment == "Steam":
self.steam_manager.update_bottle(config)
RegistryRuleManager.apply_rules(config, trigger="components")
return Result(True, data={"config": config})

def __rollback_d7vk(self, previous: BottleConfig, failure: Result) -> Result:
if not self.reconcile_d7vk(previous):
Expand Down
49 changes: 42 additions & 7 deletions bottles/backend/managers/steam.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,28 +708,44 @@ def launch_app(prefix: str):
SignalManager.send(Signals.GShowUri, Result(data=uri))

def add_shortcut(self, program_name: str, program_path: str):
logging.info(f"Adding shortcut for {program_name}")
if "FLATPAK_ID" in os.environ:
cmd = "flatpak"
args = f"run --command=bottles-cli {os.environ['FLATPAK_ID']} run -b {{0}} -p {{1}}"
else:
cmd = "bottles-cli"
args = "run -b {0} -p {1}"

return self.__add_command_shortcut(
program_name,
cmd,
args.format(
shlex.quote(self.config.Name), shlex.quote(program_name)
),
ManagerUtils.get_bottle_path(self.config),
ManagerUtils.extract_icon(self.config, program_name, program_path),
)

def __add_command_shortcut(
self,
program_name: str,
command: str,
arguments: str,
start_dir: str,
icon: str,
):
logging.info(f"Adding shortcut for {program_name}")
if self.userdata_path is None:
logging.warning("Userdata path is not set")
return Result(False)

confs = glob(os.path.join(self.userdata_path, "*/config/"))
shortcut = {
"AppName": program_name,
"Exe": cmd,
"StartDir": ManagerUtils.get_bottle_path(self.config),
"icon": ManagerUtils.extract_icon(self.config, program_name, program_path),
"Exe": command,
"StartDir": start_dir,
"icon": icon,
"ShortcutPath": "",
"LaunchOptions": args.format(
shlex.quote(self.config.Name), shlex.quote(program_name)
),
"LaunchOptions": arguments,
"IsHidden": 0,
"AllowDesktopConfig": 1,
"AllowOverlay": 1,
Expand Down Expand Up @@ -760,3 +776,22 @@ def add_shortcut(self, program_name: str, program_path: str):

logging.info(f"Added shortcut for {program_name}")
return Result(True)

def add_umu_shortcut(self, game):
program = {
"name": game.name,
"executable": game.executable.name,
"umu_game": str(game.id),
}
config = {"Name": f"UMU-{game.id}"}
command = ManagerUtils.get_desktop_entry_exec(
config, program, for_host=True
)
executable, *arguments = shlex.split(command)
return self.__add_command_shortcut(
game.name,
executable,
shlex.join(arguments),
str(game.executable.parent),
"com.usebottles.bottles",
)
13 changes: 13 additions & 0 deletions bottles/backend/utils/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,19 @@ def resolve_file_associations(value) -> tuple[list[str], list[str], list[str]]:

@staticmethod
def get_desktop_entry_exec(config, program: dict, for_host: bool = False) -> str:
umu_game = program.get("umu_game")
if umu_game:
command = "bottles-cli"
flatpak_id = os.environ.get("FLATPAK_ID")
if for_host and flatpak_id:
command = "flatpak run --command=bottles-cli {}".format(
ManagerUtils.quote_desktop_entry_exec_arg(flatpak_id)
)
return "{} umu run --game {}".format(
command,
ManagerUtils.quote_desktop_entry_exec_arg(umu_game),
)

_, mime_types, _ = ManagerUtils.resolve_file_associations(
program.get("file_extensions", [])
)
Expand Down
12 changes: 11 additions & 1 deletion bottles/backend/wine/wineboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ def send_status(self, status: int):
if status == -2:
return self.nv_stop_all_processes()

states = {-1: "force", 0: "-k", 1: "-r", 2: "-s", 3: "-u", 4: "-i", 5: "-e", 11: "-e -f -k -r", 12: "-e -f -k -s"}
states = {
-1: "force",
0: "-k",
1: "-r",
2: "-s",
3: "-u",
4: "-i",
5: "-e",
11: "-e -f -k -r",
12: "-e -f -k -s",
}
envs = {
"WINEDEBUG": "-all",
"DISPLAY": ":3.0",
Expand Down
4 changes: 1 addition & 3 deletions bottles/frontend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk # type: ignore

from bottles.frontend.utils.gtk import FontScaleManager
from bottles.frontend.views.preferences import PreferencesWindow
from bottles.frontend.windows.window import BottlesWindow

logging = Logger()
Expand Down Expand Up @@ -363,8 +362,7 @@ def __refresh(self, action=None, param=None):
self.win.manager.update_bottles()

def __show_preferences(self, *args):
preferences_window = PreferencesWindow(self.win)
preferences_window.present(self.win)
self.win.show_prefs_view()

def __new_bottle(self, *args):
self.win.show_add_view()
Expand Down
Loading
Loading