diff --git a/rare/app.py b/rare/app.py index b23aa185..4a0f292a 100644 --- a/rare/app.py +++ b/rare/app.py @@ -151,7 +151,9 @@ class App(RareApp): def start_app(self): for igame in self.core.get_installed_list(): if not os.path.exists(igame.install_path): - legendary_utils.uninstall(igame.app_name, self.core) + # lk; since install_path is lost anyway, set keep_files to True + # lk: to avoid spamming the log with "file not found" errors + legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True) logger.info(f"Uninstalled {igame.title}, because no game files exist") continue if not os.path.exists(os.path.join(igame.install_path, igame.executable.replace("\\", "/").lstrip("/"))): @@ -228,6 +230,7 @@ class App(RareApp): self.mainwindow.hide() threadpool = QThreadPool.globalInstance() threadpool.waitForDone() + self.core.exit() if self.mainwindow is not None: self.mainwindow.close() if self.tray_icon is not None: diff --git a/rare/components/dialogs/uninstall_dialog.py b/rare/components/dialogs/uninstall_dialog.py index 13caab00..4ead1380 100644 --- a/rare/components/dialogs/uninstall_dialog.py +++ b/rare/components/dialogs/uninstall_dialog.py @@ -1,3 +1,6 @@ +from enum import Enum, IntEnum +from typing import Tuple + from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, @@ -16,21 +19,20 @@ from rare.utils.utils import icon class UninstallDialog(QDialog): def __init__(self, game: Game): super(UninstallDialog, self).__init__() - self.setWindowTitle("Uninstall Game") - self.info = 0 self.setAttribute(Qt.WA_DeleteOnClose, True) - self.layout = QVBoxLayout() + self.setWindowTitle("Uninstall Game") + layout = QVBoxLayout() self.info_text = QLabel( self.tr("Do you really want to uninstall {}").format(game.app_title) ) - self.layout.addWidget(self.info_text) + layout.addWidget(self.info_text) self.keep_files = QCheckBox(self.tr("Keep Files")) - self.form = QFormLayout() - self.form.setContentsMargins(0, 10, 0, 10) - self.form.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files) - self.layout.addLayout(self.form) + form_layout = QFormLayout() + form_layout.setContentsMargins(0, 10, 0, 10) + form_layout.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files) + layout.addLayout(form_layout) - self.button_layout = QHBoxLayout() + button_layout = QHBoxLayout() self.ok_button = QPushButton( icon("ei.remove-circle", color="red"), self.tr("Uninstall") ) @@ -39,20 +41,22 @@ class UninstallDialog(QDialog): self.cancel_button = QPushButton(self.tr("Cancel")) self.cancel_button.clicked.connect(self.cancel) - self.button_layout.addStretch(1) - self.button_layout.addWidget(self.ok_button) - self.button_layout.addWidget(self.cancel_button) - self.layout.addLayout(self.button_layout) - self.setLayout(self.layout) + button_layout.addStretch(1) + button_layout.addWidget(self.ok_button) + button_layout.addWidget(self.cancel_button) + layout.addLayout(button_layout) + self.setLayout(layout) - def get_information(self): + self.options: Tuple[bool, bool] = (False, False) + + def get_options(self) -> Tuple[bool, bool]: self.exec_() - return self.info + return self.options def ok(self): - self.info = {"keep_files": self.keep_files.isChecked()} + self.options = (True, self.keep_files.isChecked()) self.close() def cancel(self): - self.info = 0 + self.options = (False, False) self.close() diff --git a/rare/components/tabs/games/game_utils.py b/rare/components/tabs/games/game_utils.py index 56b6bf1c..a31338ad 100644 --- a/rare/components/tabs/games/game_utils.py +++ b/rare/components/tabs/games/game_utils.py @@ -164,10 +164,10 @@ class GameUtils(QObject): else: return False - infos = UninstallDialog(game).get_information() - if infos == 0: + proceed, keep_files = UninstallDialog(game).get_options() + if not proceed: return False - legendary_utils.uninstall(game.app_name, self.core, infos) + legendary_utils.uninstall_game(self.core, game.app_name, keep_files) self.signals.game_uninstalled.emit(app_name) return True diff --git a/rare/lgndr/api_exception.py b/rare/lgndr/api_exception.py index f31b94c0..11779f79 100644 --- a/rare/lgndr/api_exception.py +++ b/rare/lgndr/api_exception.py @@ -14,10 +14,19 @@ class LgndrWarning(RuntimeWarning): super(LgndrWarning, self).__init__(self.message) -class LgndrLogHandler(logging.Handler): +class LgndrCLILogHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: # lk: FATAL is the same as CRITICAL if record.levelno == logging.ERROR or record.levelno == logging.CRITICAL: raise LgndrException(record.getMessage()) - # if record.levelno == logging.INFO or record.levelno == logging.WARNING: + # if record.levelno < logging.ERROR or record.levelno == logging.WARNING: + # warnings.warn(record.getMessage()) + + +class LgndrCoreLogHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + # lk: FATAL is the same as CRITICAL + if record.levelno == logging.CRITICAL: + raise LgndrException(record.getMessage()) + # if record.levelno < logging.CRITICAL: # warnings.warn(record.getMessage()) diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py index 4bccd8ea..9415ecd8 100644 --- a/rare/lgndr/cli.py +++ b/rare/lgndr/cli.py @@ -13,24 +13,17 @@ from legendary.utils.selective_dl import get_sdl_appname from .core import LegendaryCore from .manager import DLManager -from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs -from .api_exception import LgndrException, LgndrLogHandler +from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs, LgndrUninstallGameArgs +from .api_exception import LgndrException, LgndrCLILogHandler from .api_monkeys import return_exit as exit, get_boolean_choice +legendary.cli.exit = exit + class LegendaryCLI(legendary.cli.LegendaryCLI): - def __init__(self, override_config=None, api_timeout=None): - self.core = LegendaryCore(override_config, timeout=api_timeout) - self.logger = logging.getLogger('cli') - self.logging_queue = None - self.handler = LgndrLogHandler() - self.logger.addHandler(self.handler) - - def resolve_aliases(self, name): - return super(LegendaryCLI, self)._resolve_aliases(name) @staticmethod - def wrapped(func): + def apply_wrap(func): @functools.wraps(func) def inner(self, args, *oargs, **kwargs): @@ -39,6 +32,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): old_choice = legendary.cli.get_boolean_choice if hasattr(args, 'get_boolean_choice') and args.get_boolean_choice is not None: + # g['get_boolean_choice'] = args.get_boolean_choice legendary.cli.get_boolean_choice = args.get_boolean_choice try: @@ -46,12 +40,22 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): except LgndrException as ret: raise ret finally: + # g['get_boolean_choice'] = old_choice legendary.cli.get_boolean_choice = old_choice legendary.cli.exit = old_exit return inner - @wrapped + def __init__(self, override_config=None, api_timeout=None): + self.core = LegendaryCore(override_config, timeout=api_timeout) + self.logger = logging.getLogger('cli') + self.logging_queue = None + self.handler = LgndrCLILogHandler() + self.logger.addHandler(self.handler) + + def resolve_aliases(self, name): + return super(LegendaryCLI, self)._resolve_aliases(name) + def install_game(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult): args.app_name = self._resolve_aliases(args.app_name) if self.core.is_installed(args.app_name): @@ -222,15 +226,39 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): self.core.uninstall_tag(old_igame) self.core.install_game(old_igame) - @wrapped + @apply_wrap def handle_postinstall(self, postinstall, igame, yes=False): super(LegendaryCLI, self)._handle_postinstall(postinstall, igame, yes) - @wrapped - def uninstall_game(self, args): - super(LegendaryCLI, self).uninstall_game(args) + def uninstall_game(self, args: LgndrUninstallGameArgs): + args.app_name = self._resolve_aliases(args.app_name) + igame = self.core.get_installed_game(args.app_name) + if not igame: + logger.error(f'Game {args.app_name} not installed, cannot uninstall!') + exit(0) + + if not args.yes: + if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False): + return False + + try: + if not igame.is_dlc: + # Remove DLC first so directory is empty when game uninstall runs + dlcs = self.core.get_dlc_for_game(igame.app_name) + for dlc in dlcs: + if (idlc := self.core.get_installed_game(dlc.app_name)) is not None: + logger.info(f'Uninstalling DLC "{dlc.app_name}"...') + self.core.uninstall_game(idlc, delete_files=not args.keep_files) + + logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') + self.core.uninstall_game(igame, delete_files=not args.keep_files, + delete_root_directory=not igame.is_dlc) + logger.info('Game has been uninstalled.') + return True + except Exception as e: + logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.') + return False - @wrapped def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False): args.app_name = self._resolve_aliases(args.app_name) if not self.core.is_installed(args.app_name): @@ -340,6 +368,6 @@ class LegendaryCLI(legendary.cli.LegendaryCLI): logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.') return False, len(failed), len(missing) - @wrapped + @apply_wrap def import_game(self, args: LgndrImportGameArgs): super(LegendaryCLI, self).import_game(args) diff --git a/rare/lgndr/core.py b/rare/lgndr/core.py index 1f4e463d..c479855a 100644 --- a/rare/lgndr/core.py +++ b/rare/lgndr/core.py @@ -2,10 +2,10 @@ from multiprocessing import Queue import legendary.core from legendary.models.downloading import AnalysisResult -from legendary.models.game import Game +from legendary.models.game import Game, InstalledGame from legendary.models.manifest import ManifestMeta -from .api_exception import LgndrException, LgndrLogHandler +from .api_exception import LgndrException, LgndrCoreLogHandler from .manager import DLManager # lk: Monkeypatch the modified DLManager into LegendaryCore @@ -16,7 +16,7 @@ class LegendaryCore(legendary.core.LegendaryCore): def __init__(self, override_config=None, timeout=10.0): super(LegendaryCore, self).__init__(override_config=override_config, timeout=timeout) - self.handler = LgndrLogHandler() + self.handler = LgndrCoreLogHandler() self.log.addHandler(self.handler) def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '', @@ -31,7 +31,7 @@ class LegendaryCore(legendary.core.LegendaryCore): disable_delta: bool = False, override_delta_manifest: str = '', egl_guid: str = '', preferred_cdn: str = None, disable_https: bool = False) -> (DLManager, AnalysisResult, ManifestMeta): - dlm, analysis, igame = super(LegendaryCore, self).prepare_download( + return super(LegendaryCore, self).prepare_download( game=game, base_game=base_game, base_path=base_path, status_q=status_q, max_shm=max_shm, max_workers=max_workers, force=force, disable_patching=disable_patching, @@ -45,7 +45,14 @@ class LegendaryCore(legendary.core.LegendaryCore): egl_guid=egl_guid, preferred_cdn=preferred_cdn, disable_https=disable_https ) - return dlm, analysis, igame + + def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False): + try: + super(LegendaryCore, self).uninstall_game(installed_game, delete_files, delete_root_directory) + except Exception as e: + raise e + finally: + pass def egl_import(self, app_name): try: @@ -68,3 +75,4 @@ class LegendaryCore(legendary.core.LegendaryCore): # lk: monkeypatch status_q (the queue for download stats) dlm.status_queue = status_q return dlm, analysis_result, igame + diff --git a/rare/utils/legendary_utils.py b/rare/utils/legendary_utils.py index 32cf1508..358f8c40 100644 --- a/rare/utils/legendary_utils.py +++ b/rare/utils/legendary_utils.py @@ -5,7 +5,7 @@ from logging import getLogger from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths from legendary.core import LegendaryCore -from rare.lgndr.api_arguments import LgndrVerifyGameArgs +from rare.lgndr.api_arguments import LgndrVerifyGameArgs, LgndrUninstallGameArgs from rare.lgndr.api_exception import LgndrException from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton from rare.utils import config_helper @@ -13,9 +13,7 @@ from rare.utils import config_helper logger = getLogger("Legendary Utils") -def uninstall(app_name: str, core: LegendaryCore, options=None): - if not options: - options = {"keep_files": False} +def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False): igame = core.get_installed_game(app_name) # remove shortcuts link @@ -40,31 +38,22 @@ def uninstall(app_name: str, core: LegendaryCore, options=None): if os.path.exists(start_menu_shortcut): os.remove(start_menu_shortcut) - try: - # Remove DLC first so directory is empty when game uninstall runs - dlcs = core.get_dlc_for_game(app_name) - for dlc in dlcs: - if (idlc := core.get_installed_game(dlc.app_name)) is not None: - logger.info(f'Uninstalling DLC "{dlc.app_name}"...') - core.uninstall_game(idlc, delete_files=not options["keep_files"]) - - logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') - core.uninstall_game( - igame, delete_files=not options["keep_files"], delete_root_directory=True + result = LegendaryCLISingleton().uninstall_game( + LgndrUninstallGameArgs( + app_name=app_name, + keep_files=keep_files, + yes=True, ) - logger.info("Game has been uninstalled.") - - except Exception as e: - logger.warning( - f"Removing game failed: {e!r}, please remove {igame.install_path} manually." - ) - if not options["keep_files"]: + ) + if not keep_files: logger.info("Removing sections in config file") config_helper.remove_section(app_name) config_helper.remove_section(f"{app_name}.env") config_helper.save_config() + return result + def update_manifest(app_name: str, core: LegendaryCore): game = core.get_game(app_name)