1
0
Fork 0
mirror of synced 2024-06-02 18:54:41 +12:00

Mirror Legendary classes structure in the shim.

Lgndr: Move code segments copied from `prepare_download` back to their original location in `install_game`
Lgndr: Add the LgndrLogHandler at initialization instead of every function.
Lgndr: Move `verify_game` to its original place in `LegendaryCLI`
Lgndr: Change the way DLManager is patched into LegendaryCore proper
Shared: Add singleton for LegendaryCLI, LegendaryCoreSignleton returns core from LegendaryCLI
VerifyWorker: Update to use `verify_game` from `LegendaryCLI` directly
PreLaunchThread: Initialize LegendaryCLI to get LegendaryCore from it
InstallDialog: Update `prepare_install` argument names
This commit is contained in:
loathingKernel 2022-07-07 20:19:52 +03:00
parent 3892f4a594
commit 883bd268ff
9 changed files with 466 additions and 321 deletions

View file

@ -20,7 +20,7 @@ import rare
from rare.components.dialogs.launch_dialog import LaunchDialog
from rare.components.main_window import MainWindow
from rare.components.tray_icon import TrayIcon
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared.image_manager import ImageManagerSingleton
from rare.utils import legendary_utils, config_helper
from rare.utils.paths import cache_dir, tmp_dir
@ -64,7 +64,8 @@ class App(RareApp):
# init Legendary
try:
self.core = LegendaryCoreSingleton(init=True)
LegendaryCLISingleton(init=True)
self.core = LegendaryCoreSingleton()
except configparser.MissingSectionHeaderError as e:
logger.warning(f"Config is corrupt: {e}")
if config_path := os.environ.get("XDG_CONFIG_HOME"):
@ -73,7 +74,8 @@ class App(RareApp):
path = os.path.expanduser("~/.config/legendary")
with open(os.path.join(path, "config.ini"), "w") as config_file:
config_file.write("[Legendary]")
self.core = LegendaryCoreSingleton(init=True)
LegendaryCLISingleton(init=True)
self.core = LegendaryCoreSingleton()
if "Legendary" not in self.core.lgd.config.sections():
self.core.lgd.config.add_section("Legendary")
self.core.lgd.save_config()

View file

@ -7,12 +7,14 @@ from typing import Tuple
from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QKeyEvent
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.core import LegendaryCore
from legendary.models.downloading import ConditionCheckResult
from legendary.models.game import Game
from legendary.utils.selective_dl import games
from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
from rare.utils.extra_widgets import PathEdit
from rare.utils.models import InstallDownloadModel, InstallQueueItemModel
@ -306,14 +308,16 @@ class InstallInfoWorker(QRunnable):
def run(self):
try:
if not self.is_overlay_install:
cli = LegendaryCLISingleton()
download = InstallDownloadModel(
*self.core.prepare_download(
# *self.core.prepare_download(
*cli.prepare_install(
app_name=self.dl_item.options.app_name,
base_path=self.dl_item.options.base_path,
force=self.dl_item.options.force,
no_install=self.dl_item.options.no_install,
status_q=self.dl_item.status_q,
max_shm=self.dl_item.options.max_shm,
shared_memory=self.dl_item.options.max_shm,
max_workers=self.dl_item.options.max_workers,
# game_folder=,
# disable_patching=,
@ -324,11 +328,11 @@ class InstallInfoWorker(QRunnable):
# file_prefix_filter=,
# file_exclude_filter=,
# file_install_tag=,
dl_optimizations=self.dl_item.options.dl_optimizations,
order_opt=self.dl_item.options.dl_optimizations,
# dl_timeout=,
repair=self.dl_item.options.repair,
# repair_use_latest=,
ignore_space_req=self.dl_item.options.ignore_space_req,
repair_mode=self.dl_item.options.repair,
# repair_and_update=True,
ignore_space=self.dl_item.options.ignore_space_req,
# disable_delta=,
# override_delta_manifest=,
# reset_sdl=,

View file

@ -11,9 +11,8 @@ from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThrea
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.exception import LgndrException
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
from rare.utils import legendary_utils
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
@ -121,8 +120,7 @@ class ImportWorker(QRunnable):
return args
def __import_game(self, app_name: str, path: Path):
cli = LegendaryCLI()
cli.core = LegendaryCoreSingleton()
cli = LegendaryCLISingleton()
args = self.import_game_args(str(path), app_name)
try:
cli.import_game(args)

View file

@ -14,7 +14,7 @@ from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from .console import Console
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
from .message_models import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
from ..shared import LegendaryCoreSingleton
from ..shared import LegendaryCLISingleton, LegendaryCoreSingleton
from ..widgets.rare_app import RareApp
@ -69,7 +69,8 @@ class GameProcessApp(RareApp):
self.game_process = QProcess()
self.app_name = app_name
self.logger = getLogger(self.app_name)
self.core = LegendaryCoreSingleton(True)
LegendaryCLISingleton(init=True)
self.core = LegendaryCoreSingleton()
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)

View file

@ -1,11 +1,21 @@
import os
import logging
import time
from argparse import Namespace
from multiprocessing import Queue
from typing import Optional, Callable, List
import legendary.cli
from PyQt5.QtWidgets import QLabel, QMessageBox
from legendary.cli import LegendaryCLI as LegendaryCLIReal, logger
from legendary.cli import logger
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
from legendary.models.game import Game, InstalledGame, VerifyResult
from legendary.utils.lfs import validate_files
from legendary.utils.selective_dl import get_sdl_appname
from .core import LegendaryCore
from .exception import LgndrException, LgndrLogHandler
from .manager import DLManager
def get_boolean_choice(message):
@ -22,22 +32,347 @@ class UILogHandler(logging.Handler):
self.widget.setText(record.getMessage())
class LegendaryCLI(LegendaryCLIReal):
def __init__(self):
self.core = None
class LegendaryCLI(legendary.cli.LegendaryCLI):
def __init__(self, override_config=None, api_timeout=None):
self.core = LegendaryCore(override_config)
self.logger = logging.getLogger('cli')
self.logging_queue = None
self.handler = LgndrLogHandler()
self.logger.addHandler(self.handler)
# def __init__(self, core: LegendaryCore):
# self.core = core
# self.logger = logging.getLogger('cli')
# self.logging_queue = None
# self.handler = LgndrLogHandler()
# self.logger.addHandler(self.handler)
# app_name, repair, repair_mode, no_install, repair_and_update, file_prefix, file_exclude_prefix, platform
# sdl_prompt, status_q
def prepare_install(self, app_name: str, base_path: str = '',
status_q: Queue = None, shared_memory: int = 0, max_workers: int = 0,
force: bool = False, disable_patching: bool = False,
game_folder: str = '', override_manifest: str = '',
override_old_manifest: str = '', override_base_url: str = '',
platform: str = 'Windows', file_prefix: list = None,
file_exclude_prefix: list = None, install_tag: list = None,
order_opt: bool = False, dl_timeout: int = 10,
repair_mode: bool = False, repair_and_update: bool = False,
disable_delta: bool = False, override_delta_manifest: str = '',
egl_guid: str = '', preferred_cdn: str = None,
no_install: bool = False, ignore_space: bool = False,
disable_sdl: bool = False, reset_sdl: bool = False, skip_sdl: bool = False,
sdl_prompt: Callable[[str, str], List[str]] = list,
yes: bool = True, verify_callback: Callable[[int, int, float, float], None] = None,
disable_https: bool = False) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
args = Namespace(
app_name=app_name, base_path=base_path,
status_q=status_q, shared_memory=shared_memory, max_workers=max_workers,
force=force, disable_patching=disable_patching,
game_folder=game_folder, override_manifest=override_manifest,
override_old_manifest=override_old_manifest, override_base_url=override_base_url,
platform=platform, file_prefix=file_prefix,
file_exclude_prefix=file_exclude_prefix, install_tag=install_tag,
order_opt=order_opt, dl_timeout=dl_timeout,
repair_mode=repair_mode, repair_and_update=repair_and_update,
disable_delta=disable_delta, override_delta_manifest=override_delta_manifest,
preferred_cdn=preferred_cdn,
no_install=no_install, ignore_space=ignore_space,
disable_sdl=disable_sdl, reset_sdl=reset_sdl, skip_sdl=skip_sdl,
sdl_prompt=sdl_prompt,
yes=yes, callback=verify_callback,
disable_https=disable_https
)
old_choice = legendary.cli.get_boolean_choice
legendary.cli.get_boolean_choice = get_boolean_choice
try:
return self.install_game(args)
except LgndrException as ret:
raise ret
finally:
legendary.cli.get_boolean_choice = old_choice
def install_game(self, args):
args.app_name = self._resolve_aliases(args.app_name)
if self.core.is_installed(args.app_name):
igame = self.core.get_installed_game(args.app_name)
args.platform = igame.platform
if igame.needs_verification and not args.repair_mode:
logger.info('Game needs to be verified before updating, switching to repair mode...')
args.repair_mode = True
repair_file = None
if args.repair_mode:
args.repair_mode = True
args.no_install = args.repair_and_update is False
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
if args.file_prefix or args.file_exclude_prefix:
args.no_install = True
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
if not game:
logger.error(f'Could not find "{args.app_name}" in list of available games, '
f'did you type the name correctly?')
if args.platform not in game.asset_infos:
if not args.no_install:
if self.core.lgd.config.getboolean('Legendary', 'install_platform_fallback', fallback=True):
logger.warning(f'App has no asset for platform "{args.platform}", falling back to "Windows".')
args.platform = 'Windows'
else:
logger.error(f'No app asset found for platform "{args.platform}", run '
f'"legendary info --platform {args.platform}" and make '
f'sure the app is available for the specified platform.')
return
else:
logger.warning(f'No asset found for platform "{args.platform}", '
f'trying anyway since --no-install is set.')
if game.is_dlc:
logger.info('Install candidate is DLC')
app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
base_game = self.core.get_game(app_name)
# check if base_game is actually installed
if not self.core.is_installed(app_name):
# download mode doesn't care about whether something's installed
if not args.no_install:
logger.fatal(f'Base game "{app_name}" is not installed!')
else:
base_game = None
if args.repair_mode:
if not self.core.is_installed(game.app_name):
logger.error(f'Game "{game.app_title}" ({game.app_name}) is not installed!')
if not os.path.exists(repair_file):
logger.info('Game has not been verified yet.')
if not args.yes:
if not get_boolean_choice(f'Verify "{game.app_name}" now ("no" will abort repair)?'):
return
try:
self.verify_game(args, print_command=False, repair_mode=True, repair_online=args.repair_and_update)
except ValueError:
logger.error('To repair a game with a missing manifest you must run the command with '
'"--repair-and-update". However this will redownload any file that does '
'not match the current hash in its entirety.')
return
else:
logger.info(f'Using existing repair file: {repair_file}')
# check if SDL should be disabled
sdl_enabled = not args.install_tag and not game.is_dlc
config_tags = self.core.lgd.config.get(game.app_name, 'install_tags', fallback=None)
config_disable_sdl = self.core.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False)
# remove config flag if SDL is reset
if config_disable_sdl and args.reset_sdl and not args.disable_sdl:
self.core.lgd.config.remove_option(game.app_name, 'disable_sdl')
# if config flag is not yet set, set it and remove previous install tags
elif not config_disable_sdl and args.disable_sdl:
logger.info('Clearing install tags from config and disabling SDL for title.')
if config_tags:
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
config_tags = None
self.core.lgd.config.set(game.app_name, 'disable_sdl', True)
sdl_enabled = False
# just disable SDL, but keep config tags that have been manually specified
elif config_disable_sdl or args.disable_sdl:
sdl_enabled = False
if sdl_enabled and ((sdl_name := get_sdl_appname(game.app_name)) is not None):
if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl:
sdl_data = self.core.get_sdl_data(sdl_name, platform=args.platform)
if sdl_data:
if args.skip_sdl:
args.install_tag = ['']
if '__required' in sdl_data:
args.install_tag.extend(sdl_data['__required']['tags'])
else:
args.install_tag = args.sdl_prompt(sdl_data, game.app_title)
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
else:
logger.error(f'Unable to get SDL data for {sdl_name}')
else:
args.install_tag = config_tags.split(',')
elif args.install_tag and not game.is_dlc and not args.no_install:
config_tags = ','.join(args.install_tag)
logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
self.core.lgd.config.set(game.app_name, 'install_tags', config_tags)
elif not game.is_dlc:
if config_tags and args.reset_sdl:
logger.info('Clearing install tags from config.')
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
elif config_tags:
logger.info(f'Using install tags from config: {config_tags}')
args.install_tag = config_tags.split(',')
logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...')
# todo use status queue to print progress from CLI
# This has become a little ridiculous hasn't it?
dlm, analysis, igame = self.core.prepare_download(game=game, base_game=base_game, base_path=args.base_path,
status_q=args.status_q,
force=args.force, max_shm=args.shared_memory,
max_workers=args.max_workers, game_folder=args.game_folder,
disable_patching=args.disable_patching,
override_manifest=args.override_manifest,
override_old_manifest=args.override_old_manifest,
override_base_url=args.override_base_url,
platform=args.platform,
file_prefix_filter=args.file_prefix,
file_exclude_filter=args.file_exclude_prefix,
file_install_tag=args.install_tag,
dl_optimizations=args.order_opt,
dl_timeout=args.dl_timeout,
repair=args.repair_mode,
repair_use_latest=args.repair_and_update,
disable_delta=args.disable_delta,
override_delta_manifest=args.override_delta_manifest,
preferred_cdn=args.preferred_cdn,
disable_https=args.disable_https)
# game is either up-to-date or hasn't changed, so we have nothing to do
if not analysis.dl_size:
logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...')
self.clean_post_install(game, igame, args.repair_mode, repair_file)
logger.error('Nothing to do.')
res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game,
updating=self.core.is_installed(args.app_name),
ignore_space_req=args.ignore_space)
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
def clean_post_install(self, game: Game, igame: InstalledGame, repair: bool = False, repair_file: str = ''):
old_igame = self.core.get_installed_game(game.app_name)
if old_igame and repair and os.path.exists(repair_file):
if old_igame.needs_verification:
old_igame.needs_verification = False
self.core.install_game(old_igame)
logger.debug('Removing repair file.')
os.remove(repair_file)
# check if install tags have changed, if they did; try deleting files that are no longer required.
if old_igame and old_igame.install_tags != igame.install_tags:
old_igame.install_tags = igame.install_tags
logger.info('Deleting now untagged files.')
self.core.uninstall_tag(old_igame)
self.core.install_game(old_igame)
def verify_game(self, args, print_command=True, repair_mode=False, repair_online=False):
if not hasattr(args, 'callback') or args.callback is None:
args.callback = print
args.app_name = self._resolve_aliases(args.app_name)
if not self.core.is_installed(args.app_name):
logger.error(f'Game "{args.app_name}" is not installed')
return
logger.info(f'Loading installed manifest for "{args.app_name}"')
igame = self.core.get_installed_game(args.app_name)
if not os.path.exists(igame.install_path):
logger.error(f'Install path "{igame.install_path}" does not exist, make sure all necessary mounts '
f'are available. If you previously deleted the game folder without uninstalling, run '
f'"legendary uninstall -y {igame.app_name}" and reinstall from scratch.')
return
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
if manifest_data is None:
if repair_mode:
if not repair_online:
logger.critical('No manifest could be loaded, the manifest file may be missing!')
raise ValueError('Local manifest is missing')
logger.warning('No manifest could be loaded, the file may be missing. Downloading the latest manifest.')
game = self.core.get_game(args.app_name, platform=igame.platform)
manifest_data, _ = self.core.get_cdn_manifest(game, igame.platform)
else:
logger.critical(f'Manifest appears to be missing! To repair, run "legendary repair '
f'{args.app_name} --repair-and-update", this will however redownload all files '
f'that do not match the latest manifest in their entirety.')
return
manifest = self.core.load_manifest(manifest_data)
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
# build list of hashes
if config_tags := self.core.lgd.config.get(args.app_name, 'install_tags', fallback=None):
install_tags = set(i.strip() for i in config_tags.split(','))
file_list = [
(f.filename, f.sha_hash.hex())
for f in files
if any(it in install_tags for it in f.install_tags) or not f.install_tags
]
else:
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
total = len(file_list)
total_size = sum(manifest.file_manifest_list.get_file_by_path(fm[0]).file_size
for fm in file_list)
num = processed = last_processed = 0
speed = 0.0
percentage = 0.0
failed = []
missing = []
last_update = time.time()
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
repair_file = []
for result, path, result_hash, bytes_read in validate_files(igame.install_path, file_list):
processed += bytes_read
percentage = (processed / total_size) * 100.0
num += 1
if (delta := ((current_time := time.time()) - last_update)) > 1 or (not last_processed and delta > 1):
last_update = current_time
speed = (processed - last_processed) / 1024 / 1024 / delta
last_processed = processed
if args.callback:
args.callback(num, total, percentage, speed)
if result == VerifyResult.HASH_MATCH:
repair_file.append(f'{result_hash}:{path}')
continue
elif result == VerifyResult.HASH_MISMATCH:
logger.info(f'File does not match hash: "{path}"')
repair_file.append(f'{result_hash}:{path}')
failed.append(path)
elif result == VerifyResult.FILE_MISSING:
logger.info(f'File is missing: "{path}"')
missing.append(path)
else:
logger.info(f'Other failure (see log), treating file as missing: "{path}"')
missing.append(path)
if args.callback:
args.callback(num, total, percentage, speed)
# always write repair file, even if all match
if repair_file:
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
with open(repair_filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(repair_file))
logger.debug(f'Written repair file to "{repair_filename}"')
if not missing and not failed:
logger.info('Verification finished successfully.')
else:
logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
if print_command:
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
def import_game(self, args):
handler = LgndrLogHandler()
logger.addHandler(handler)
old_choice = legendary.cli.get_boolean_choice
legendary.cli.get_boolean_choice = get_boolean_choice
try:
super(LegendaryCLI, self).import_game(args)
except LgndrException as e:
raise e
except LgndrException as ret:
raise ret
finally:
legendary.cli.get_boolean_choice = old_choice
logger.removeHandler(handler)

View file

@ -1,24 +1,22 @@
import logging
import os
import sys
from multiprocessing import Queue
from typing import Callable
from legendary.utils.lfs import validate_files
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
from legendary.models.game import *
from legendary.utils.selective_dl import get_sdl_appname
import legendary.core
from legendary.models.downloading import AnalysisResult
from legendary.models.game import Game
from legendary.models.manifest import ManifestMeta
from legendary.core import LegendaryCore as LegendaryCoreReal
from .manager import DLManager
from .exception import LgndrException, LgndrLogHandler
from .manager import DLManager
class LegendaryCore(LegendaryCoreReal):
# fmt: off
def prepare_download(self, app_name: str, base_path: str = '',
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.log.addHandler(self.handler)
def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '',
status_q: Queue = None, max_shm: int = 0, max_workers: int = 0,
force: bool = False, disable_patching: bool = False,
game_folder: str = '', override_manifest: str = '',
@ -29,232 +27,59 @@ class LegendaryCore(LegendaryCoreReal):
repair: bool = False, repair_use_latest: bool = False,
disable_delta: bool = False, override_delta_manifest: str = '',
egl_guid: str = '', preferred_cdn: str = None,
no_install: bool = False, ignore_space_req: bool = False,
disable_sdl: bool = False, reset_sdl: bool = False, skip_sdl: bool = False,
sdl_prompt: Callable[[str, str], List[str]] = list,
disable_https: bool = False) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
if self.is_installed(app_name):
igame = self.get_installed_game(app_name)
platform = igame.platform
if igame.needs_verification and not repair:
self.log.info('Game needs to be verified before updating, switching to repair mode...')
repair = True
repair_file = None
if repair:
repair = True
no_install = repair_use_latest is False
repair_file = os.path.join(self.lgd.get_tmp_path(), f'{app_name}.repair')
if file_prefix_filter or file_exclude_filter or file_install_tag:
no_install = True
game = self.get_game(app_name, update_meta=True)
if not game:
raise RuntimeError(f'Could not find "{app_name}" in list of available games,'
f'did you type the name correctly?')
if game.is_dlc:
self.log.info('Install candidate is DLC')
app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
base_game = self.get_game(app_name)
# check if base_game is actually installed
if not self.is_installed(app_name):
# download mode doesn't care about whether or not something's installed
if not no_install:
raise RuntimeError(f'Base game "{app_name}" is not installed!')
else:
base_game = None
if repair:
if not self.is_installed(game.app_name):
raise RuntimeError(f'Game "{game.app_title}" ({game.app_name}) is not installed!')
if not os.path.exists(repair_file):
self.log.info('Verifing game...')
self.verify_game(app_name)
else:
self.log.info(f'Using existing repair file: {repair_file}')
# check if SDL should be disabled
sdl_enabled = not file_install_tag and not game.is_dlc
config_tags = self.lgd.config.get(game.app_name, 'install_tags', fallback=None)
config_disable_sdl = self.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False)
# remove config flag if SDL is reset
if config_disable_sdl and reset_sdl and not disable_sdl:
self.lgd.config.remove_option(game.app_name, 'disable_sdl')
# if config flag is not yet set, set it and remove previous install tags
elif not config_disable_sdl and disable_sdl:
self.log.info('Clearing install tags from config and disabling SDL for title.')
if config_tags:
self.lgd.config.remove_option(game.app_name, 'install_tags')
config_tags = None
self.lgd.config.set(game.app_name, 'disable_sdl', True)
sdl_enabled = False
# just disable SDL, but keep config tags that have been manually specified
elif config_disable_sdl or disable_sdl:
sdl_enabled = False
if sdl_enabled and ((sdl_name := get_sdl_appname(game.app_name)) is not None):
if not self.is_installed(game.app_name) or config_tags is None or reset_sdl:
sdl_data = self.get_sdl_data(sdl_name, platform=platform)
if sdl_data:
if skip_sdl:
file_install_tag = ['']
if '__required' in sdl_data:
file_install_tag.extend(sdl_data['__required']['tags'])
else:
file_install_tag = sdl_prompt(sdl_data, game.app_title)
self.lgd.config.set(game.app_name, 'install_tags', ','.join(file_install_tag))
else:
self.log.error(f'Unable to get SDL data for {sdl_name}')
else:
file_install_tag = config_tags.split(',')
elif file_install_tag and not game.is_dlc and not no_install:
config_tags = ','.join(file_install_tag)
self.log.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
self.lgd.config.set(game.app_name, 'install_tags', config_tags)
elif not game.is_dlc:
if config_tags and reset_sdl:
self.log.info('Clearing install tags from config.')
self.lgd.config.remove_option(game.app_name, 'install_tags')
elif config_tags:
self.log.info(f'Using install tags from config: {config_tags}')
file_install_tag = config_tags.split(',')
dlm, analysis, igame = super(LegendaryCore, self).prepare_download(game=game, base_game=base_game, base_path=base_path,
force=force, max_shm=max_shm,
max_workers=max_workers, game_folder=game_folder,
disable_patching=disable_patching,
override_manifest=override_manifest,
override_old_manifest=override_old_manifest,
override_base_url=override_base_url,
platform=platform,
file_prefix_filter=file_prefix_filter,
file_exclude_filter=file_exclude_filter,
file_install_tag=file_install_tag,
dl_optimizations=dl_optimizations,
dl_timeout=dl_timeout,
repair=repair,
repair_use_latest=repair_use_latest,
disable_delta=disable_delta,
override_delta_manifest=override_delta_manifest,
preferred_cdn=preferred_cdn,
status_q=status_q,
disable_https=disable_https)
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
# game is either up to date or hasn't changed, so we have nothing to do
if not analysis.dl_size:
self.log.info('Download size is 0, the game is either already up to date or has not changed. Exiting...')
self.clean_post_install(game, igame, repair, repair_file)
raise RuntimeError('Nothing to do.')
res = self.check_installation_conditions(analysis=analysis, install=igame, game=game,
updating=self.is_installed(app_name),
ignore_space_req=ignore_space_req)
return dlm, analysis, igame, game, repair, repair_file, res
def verify_game(self, app_name: str, callback: Callable[[int, int], None] = print):
if not self.is_installed(app_name):
self.log.error(f'Game "{app_name}" is not installed')
return
self.log.info(f'Loading installed manifest for "{app_name}"')
igame = self.get_installed_game(app_name)
manifest_data, _ = self.get_installed_manifest(app_name)
manifest = self.load_manifest(manifest_data)
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
# build list of hashes
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
total = len(file_list)
num = 0
failed = []
missing = []
self.log.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
repair_file = []
for result, path, result_hash in validate_files(igame.install_path, file_list):
if callback:
num += 1
callback(num, total)
if result == VerifyResult.HASH_MATCH:
repair_file.append(f'{result_hash}:{path}')
continue
elif result == VerifyResult.HASH_MISMATCH:
self.log.error(f'File does not match hash: "{path}"')
repair_file.append(f'{result_hash}:{path}')
failed.append(path)
elif result == VerifyResult.FILE_MISSING:
self.log.error(f'File is missing: "{path}"')
missing.append(path)
else:
self.log.error(f'Other failure (see log), treating file as missing: "{path}"')
missing.append(path)
# always write repair file, even if all match
if repair_file:
repair_filename = os.path.join(self.lgd.get_tmp_path(), f'{app_name}.repair')
with open(repair_filename, 'w') as f:
f.write('\n'.join(repair_file))
self.log.debug(f'Written repair file to "{repair_filename}"')
if not missing and not failed:
self.log.info('Verification finished successfully.')
else:
raise RuntimeError(
f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
def clean_post_install(self, game: Game, igame: InstalledGame, repair: bool = False, repair_file: str = ''):
old_igame = self.get_installed_game(game.app_name)
if old_igame and repair and os.path.exists(repair_file):
if old_igame.needs_verification:
old_igame.needs_verification = False
self.install_game(old_igame)
self.log.debug('Removing repair file.')
os.remove(repair_file)
# check if install tags have changed, if they did; try deleting files that are no longer required.
if old_igame and old_igame.install_tags != igame.install_tags:
old_igame.install_tags = igame.install_tags
self.log.info('Deleting now untagged files.')
self.uninstall_tag(old_igame)
self.install_game(old_igame)
# fmt: on
disable_https: bool = False) -> (DLManager, AnalysisResult, ManifestMeta):
_dlmanager = legendary.core.DLManager
legendary.core.DLManager = DLManager
try:
dlm, analysis, igame = 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,
game_folder=game_folder, override_manifest=override_manifest,
override_old_manifest=override_old_manifest, override_base_url=override_base_url,
platform=platform, file_prefix_filter=file_prefix_filter,
file_exclude_filter=file_exclude_filter, file_install_tag=file_install_tag,
dl_optimizations=dl_optimizations, dl_timeout=dl_timeout,
repair=repair, repair_use_latest=repair_use_latest,
disable_delta=disable_delta, override_delta_manifest=override_delta_manifest,
egl_guid=egl_guid, preferred_cdn=preferred_cdn,
disable_https=disable_https
)
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
# dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
return dlm, analysis, igame
except LgndrException as ret:
raise ret
finally:
legendary.core.DLManager = _dlmanager
def egl_import(self, app_name):
handler = LgndrLogHandler()
self.log.addHandler(handler)
try:
super(LegendaryCore, self).egl_import(app_name)
except LgndrException as ret:
raise ret
finally:
self.log.removeHandler(handler)
pass
def egl_export(self, app_name):
handler = LgndrLogHandler()
self.log.addHandler(handler)
try:
super(LegendaryCore, self).egl_export(app_name)
except LgndrException as ret:
raise ret
finally:
self.log.removeHandler(handler)
pass
def prepare_overlay_install(self, path=None, status_q: Queue = None):
dlm, analysis_result, igame = super(LegendaryCore, self).prepare_overlay_install(path)
# lk: monkeypatch status_q (the queue for download stats)
# lk: and run_real (the method that emits the stats) into DLManager
dlm.status_queue = status_q
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
return dlm, analysis_result, igame
_dlmanager = legendary.core.DLManager
legendary.core.DLManager = DLManager
try:
dlm, analysis_result, igame = super(LegendaryCore, self).prepare_overlay_install(path)
# lk: monkeypatch status_q (the queue for download stats)
# lk: and run_real (the method that emits the stats) into DLManager
dlm.status_queue = status_q
# dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
return dlm, analysis_result, igame
except LgndrException as ret:
raise ret
finally:
legendary.core.DLManager = _dlmanager

View file

@ -10,13 +10,12 @@ from threading import Condition, Thread
from legendary.downloader.mp.workers import DLWorker, FileWorker
from legendary.models.downloading import ChunkTask, SharedMemorySegment, TerminateWorkerTask
from legendary.downloader.mp.manager import DLManager as DLManagerReal
import legendary.downloader.mp.manager
from .downloading import UIUpdate
class DLManager(DLManagerReal):
class DLManager(legendary.downloader.mp.manager.DLManager):
# fmt: off
@staticmethod
def run_real(self):
self.shared_memory = SharedMemory(create=True, size=self.max_shared_memory)
self.log.debug(f'Created shared memory of size: {self.shared_memory.size / 1024 / 1024:.02f} MiB')

View file

@ -8,24 +8,35 @@ and only ONCE!
from argparse import Namespace
from typing import Optional
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.core import LegendaryCore
from rare.models.apiresults import ApiResults
from rare.models.signals import GlobalSignals
_legendary_cli_signleton: Optional[LegendaryCLI] = None
_legendary_core_singleton: Optional[LegendaryCore] = None
_global_signals_singleton: Optional[GlobalSignals] = None
_arguments_singleton: Optional[Namespace] = None
_api_results_singleton: Optional[ApiResults] = None
def LegendaryCLISingleton(init: bool = False) -> LegendaryCLI:
global _legendary_cli_signleton
if _legendary_cli_signleton is None and not init:
raise RuntimeError("Uninitialized use of LegendaryCLISingleton")
if _legendary_cli_signleton is None:
_legendary_cli_signleton = LegendaryCLI()
return _legendary_cli_signleton
def LegendaryCoreSingleton(init: bool = False) -> LegendaryCore:
global _legendary_core_singleton
if _legendary_core_singleton is None and not init:
raise RuntimeError("Uninitialized use of LegendaryCoreSingleton")
if _legendary_core_singleton is None:
_legendary_core_singleton = LegendaryCore()
return _legendary_core_singleton
global _legendary_cli_signleton
if _legendary_cli_signleton is None:
raise RuntimeError("LegendaryCLI is not initialized yet")
# if _legendary_cli_signleton is None:
# _legendary_cli_signleton = LegendaryCLISingleton(init)
return _legendary_cli_signleton.core
def GlobalSignalsSingleton(init: bool = False) -> GlobalSignals:

View file

@ -1,14 +1,17 @@
import os
import platform
from argparse import Namespace
from logging import getLogger
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths
from legendary.core import LegendaryCore
from legendary.models.game import VerifyResult
from legendary.utils.lfs import validate_files
from parse import parse
from rare.shared import LegendaryCoreSingleton
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton
from rare.utils import config_helper
from rare.lgndr.exception import LgndrException
logger = getLogger("Legendary Utils")
@ -83,75 +86,42 @@ def update_manifest(app_name: str, core: LegendaryCore):
)
class VerifySignals(QObject):
status = pyqtSignal(int, int, str)
summary = pyqtSignal(int, int, str)
class VerifyWorker(QRunnable):
class Signals(QObject):
status = pyqtSignal(int, int, str)
summary = pyqtSignal(int, int, str)
num: int = 0
total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized
def __init__(self, app_name):
super(VerifyWorker, self).__init__()
self.signals = VerifySignals()
self.signals = VerifyWorker.Signals()
self.setAutoDelete(True)
self.cli = LegendaryCLISingleton()
self.core = LegendaryCoreSingleton()
self.app_name = app_name
def status_callback(self, num: int, total: int, percentage: float, speed: float):
self.signals.status.emit(num, total, self.app_name)
def run(self):
if not self.core.is_installed(self.app_name):
logger.error(f'Game "{self.app_name}" is not installed')
return
logger.info(f'Loading installed manifest for "{self.app_name}"')
igame = self.core.get_installed_game(self.app_name)
manifest_data, _ = self.core.get_installed_manifest(self.app_name)
manifest = self.core.load_manifest(manifest_data)
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
# build list of hashes
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
total = len(file_list)
num = 0
failed = []
missing = []
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
repair_file = []
for result, path, result_hash, _ in validate_files(igame.install_path, file_list):
num += 1
self.signals.status.emit(num, total, self.app_name)
if result == VerifyResult.HASH_MATCH:
repair_file.append(f"{result_hash}:{path}")
continue
elif result == VerifyResult.HASH_MISMATCH:
logger.error(f'File does not match hash: "{path}"')
repair_file.append(f"{result_hash}:{path}")
failed.append(path)
elif result == VerifyResult.FILE_MISSING:
logger.error(f'File is missing: "{path}"')
missing.append(path)
args = Namespace(app_name=self.app_name,
callback=self.status_callback)
try:
# TODO: offer this as an alternative when manifest doesn't exist
# TODO: requires the client to be online. To do it this way, we need to
# TODO: somehow detect the error and offer a dialog in which case `verify_games` is
# TODO: re-run with `repair_mode` and `repair_online`
self.cli.verify_game(args, print_command=False, repair_mode=True, repair_online=True)
# self.cli.verify_game(args, print_command=False)
self.signals.summary.emit(0, 0, self.app_name)
except LgndrException as ret:
r = parse('Verification failed, {:d} file(s) corrupted, {:d} file(s) are missing.', ret.message)
if r is None:
raise ret
else:
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
missing.append(path)
# always write repair file, even if all match
if repair_file:
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{self.app_name}.repair')
with open(repair_filename, 'w') as f:
f.write('\n'.join(repair_file))
logger.debug(f'Written repair file to "{repair_filename}"')
if not missing and not failed:
logger.info('Verification finished successfully.')
else:
logger.warning(
f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
self.signals.summary.emit(len(failed), len(missing), self.app_name)
self.signals.summary.emit(r[0], r[1], self.app_name)
# FIXME: lk: ah ef me sideways, we can't even import this thing properly