1
0
Fork 0
mirror of synced 2024-07-03 05:31:23 +12:00
Rare/rare/components/tabs/downloads/thread.py
loathingKernel 1721677e33 GameWidget: Implement reactive and interactive labels
The `status_label` displays what is currently going on with the game.
It reflects the current operation running on it or if it requires special
attention (update, needs verification etc)

The `tooltip_label` displays hover information such as what happens
if a part of the widget is clicked or in the case of the launch button if
the game can run (without version check, offline etc)

The context menu on the widgets will be updated and populated according
to the installation state of the game. Since the context menu was revised
the shortcut creation code was revised too to make it more compact.

the `create_desktop_link` and `get_rare_executable` functions are moved
from `rare.utils.misc` to `rare.utils.paths` to avoid cyclical imports and
better grouping. Two functions are added, `desktop_link_path` to uniformly
calculate the path of the shortcut and `desktop_links_supported` which
checks if Rare supports creating shortcuts on the current platform.
`desktop_links_supported` should be used as safeguard before `desktop_link_path`.

Desktop links are currently untested on Windows but if `shortcut.Description`
works as expected, it should be good to go.
2023-02-07 13:41:59 +02:00

185 lines
7.4 KiB
Python

import os
import platform
import queue
import time
from ctypes import c_uint64
from dataclasses import dataclass
from enum import IntEnum
from logging import getLogger
from typing import List, Optional, Dict
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.core import LegendaryCore
from rare.lgndr.glue.monkeys import DLManagerSignals
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallQueueItemModel, InstallOptionsModel
logger = getLogger("DownloadThread")
class DlResultCode(IntEnum):
ERROR = 1
STOPPED = 2
FINISHED = 3
@dataclass
class DlResultModel:
options: InstallOptionsModel
code: DlResultCode = DlResultCode.ERROR
message: str = ""
dlcs: Optional[List[Dict]] = None
sync_saves: bool = False
tip_url: str = ""
shortcut: bool = False
shortcut_name: str = ""
shortcut_title: str = ""
class DlThread(QThread):
result = pyqtSignal(DlResultModel)
progress = pyqtSignal(UIUpdate, c_uint64)
def __init__(self, item: InstallQueueItemModel, rgame: RareGame, core: LegendaryCore, debug: bool = False):
super(DlThread, self).__init__()
self.dlm_signals: DLManagerSignals = DLManagerSignals()
self.core: LegendaryCore = core
self.item: InstallQueueItemModel = item
self.dl_size = c_uint64(item.download.analysis.dl_size)
self.rgame = rgame
self.debug = debug
def __result_emit(self, result):
if result.code == DlResultCode.FINISHED:
self.rgame.set_installed(True)
self.rgame.state = RareGame.State.IDLE
self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED)
self.result.emit(result)
def run(self):
cli = LegendaryCLI(self.core)
self.item.download.dlm.logging_queue = cli.logging_queue
self.item.download.dlm.proc_debug = self.debug
result = DlResultModel(self.item.options)
start_t = time.time()
try:
self.item.download.dlm.start()
self.rgame.state = RareGame.State.DOWNLOADING
self.rgame.signals.progress.start.emit()
time.sleep(1)
while self.item.download.dlm.is_alive():
try:
status = self.item.download.dlm.status_queue.get(timeout=1.0)
self.rgame.signals.progress.update.emit(int(status.progress))
self.progress.emit(status, self.dl_size)
except queue.Empty:
pass
if self.dlm_signals.update:
try:
self.item.download.dlm.signals_queue.put(self.dlm_signals, block=False, timeout=1.0)
except queue.Full:
pass
time.sleep(self.item.download.dlm.update_interval / 10)
self.item.download.dlm.join()
except Exception as e:
self.kill()
self.item.download.dlm.join()
end_t = time.time()
logger.error(f"Installation failed after {end_t - start_t:.02f} seconds.")
logger.warning(f"The following exception occurred while waiting for the downloader to finish: {e!r}.")
result.code = DlResultCode.ERROR
result.message = f"{e!r}"
self.__result_emit(result)
return
else:
end_t = time.time()
if self.dlm_signals.kill is True:
logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.")
result.code = DlResultCode.STOPPED
self.__result_emit(result)
return
logger.info(f"Download finished in {end_t - start_t:.02f} seconds.")
result.code = DlResultCode.FINISHED
if self.item.options.overlay:
self.core.finish_overlay_install(self.item.download.igame)
self.__result_emit(result)
return
if not self.item.options.no_install:
postinstall = self.core.install_game(self.item.download.igame)
if postinstall:
# LegendaryCLI(self.core)._handle_postinstall(
# postinstall,
# self.item.download.igame,
# False,
# self.item.options.install_prereqs,
# )
self._handle_postinstall(postinstall, self.item.download.igame)
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
if dlcs and not self.item.options.skip_dlcs:
result.dlcs = []
for dlc in dlcs:
result.dlcs.append(
{
"app_name": dlc.app_name,
"app_title": dlc.app_title,
"app_version": dlc.app_version(self.item.options.platform),
}
)
if (
self.item.download.game.supports_cloud_saves
or self.item.download.game.supports_mac_cloud_saves
) and not self.item.download.game.is_dlc:
result.sync_saves = True
# show tip again after installation finishes so users hopefully actually see it
if tip_url := self.core.get_game_tip(self.item.download.igame.app_name):
result.tip_url = tip_url
LegendaryCLI(self.core).install_game_cleanup(
self.item.download.game,
self.item.download.igame,
self.item.download.repair,
self.item.download.repair_file,
)
if not self.item.options.update and self.item.options.create_shortcut:
result.shortcut = True
result.shortcut_name = self.rgame.folder_name
result.shortcut_title = self.rgame.app_title
self.__result_emit(result)
def _handle_postinstall(self, postinstall, igame):
logger.info("This game lists the following prerequisites to be installed:")
logger.info(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
if platform.system() == "Windows":
if self.item.options.install_prereqs:
logger.info("Launching prerequisite executable..")
self.core.prereq_installed(igame.app_name)
req_path, req_exec = os.path.split(postinstall["path"])
work_dir = os.path.join(igame.install_path, req_path)
fullpath = os.path.join(work_dir, req_exec)
proc = QProcess()
proc.setProcessChannelMode(QProcess.MergedChannels)
proc.readyReadStandardOutput.connect(
lambda: logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))
)
proc.setProgram(fullpath)
proc.setArguments(postinstall.get("args", "").split(" "))
proc.setWorkingDirectory(work_dir)
proc.start()
proc.waitForFinished() # wait, because it is inside the thread
else:
logger.info("Automatic installation not available on Linux.")
def kill(self):
self.dlm_signals.kill = True