Skip to content

Commit

Permalink
Lgndr: Import legendary changes
Browse files Browse the repository at this point in the history
  • Loading branch information
loathingKernel committed Dec 1, 2023
1 parent 3285971 commit 821c6e1
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 9 deletions.
90 changes: 86 additions & 4 deletions rare/lgndr/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import logging
import os
import queue
Expand All @@ -6,6 +7,7 @@
from typing import Optional, Union, Tuple

from legendary.cli import LegendaryCLI as LegendaryCLIReal
from legendary.lfs.wine_helpers import case_insensitive_file_search
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
from legendary.models.game import Game, InstalledGame, VerifyResult
from legendary.lfs.utils import validate_files
Expand All @@ -30,21 +32,35 @@ class LegendaryCLI(LegendaryCLIReal):
# noinspection PyMissingConstructor
def __init__(self, core: LegendaryCore):
self.core = core
self.logger = logging.getLogger('Cli')
self.logger = logging.getLogger('cli')
self.logging_queue = None
self.ql = self.setup_threaded_logging()

def __del__(self):
self.ql.stop()

@staticmethod
def unlock_installed(func):
@functools.wraps(func)
def unlock(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
self.core.lgd._installed_lock.release(force=True)
return ret
return unlock

def resolve_aliases(self, name):
return super(LegendaryCLI, self)._resolve_aliases(name)

@unlock_installed
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
get_boolean_choice = args.get_boolean_choice
sdl_prompt = args.sdl_prompt
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return

args.app_name = self._resolve_aliases(args.app_name)
if self.core.is_installed(args.app_name):
Expand Down Expand Up @@ -131,7 +147,7 @@ def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager,
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)
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:
Expand Down Expand Up @@ -185,7 +201,8 @@ def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager,
disable_delta=args.disable_delta,
override_delta_manifest=args.override_delta_manifest,
preferred_cdn=args.preferred_cdn,
disable_https=args.disable_https)
disable_https=args.disable_https,
bind_ip=args.bind_ip)

# game is either up-to-date or hasn't changed, so we have nothing to do
if not analysis.dl_size and not game.is_dlc:
Expand All @@ -205,10 +222,15 @@ def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager,
return dlm, analysis, igame, game, args.repair_mode, repair_file, res

# Rare: This is currently handled in DownloadThread, this is a trial
@unlock_installed
def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game: Game, igame: InstalledGame) -> LgndrInstallGameRealRet:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
ret = LgndrInstallGameRealRet(game.app_name)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return ret

start_t = time.time()

Expand Down Expand Up @@ -286,9 +308,14 @@ def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game

return ret

@unlock_installed
def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return

old_igame = self.core.get_installed_game(game.app_name)
if old_igame and repair_mode and os.path.exists(repair_file):
Expand All @@ -310,6 +337,11 @@ def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bo
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(old_igame.install_tags))
self.core.lgd.save_config()

# check if the version changed, this can happen for DLC that gets a version bump with no actual file changes
if old_igame and old_igame.version != igame.version:
old_igame.version = igame.version
self.core.install_game(old_igame)

def _handle_postinstall(self, postinstall, igame, skip_prereqs=False, choice=True):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
Expand Down Expand Up @@ -347,10 +379,16 @@ def input(x): return 'y' if choice else 'n'
else:
logger.info('Automatic installation not available on Linux.')

@unlock_installed
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
get_boolean_choice = args.get_boolean_choice
get_boolean_choice = args.get_boolean_choice_main
# def get_boolean_choice(x, default): return True
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return

args.app_name = self._resolve_aliases(args.app_name)
igame = self.core.get_installed_game(args.app_name)
Expand All @@ -362,6 +400,9 @@ def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
return

if os.name == 'nt' and igame.uninstaller and not args.skip_uninstaller:
self._handle_uninstaller(igame, args)

try:
if not igame.is_dlc:
# Remove DLC first so directory is empty when game uninstall runs
Expand All @@ -380,6 +421,31 @@ def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
return

def _handle_uninstaller(self, igame: InstalledGame, args: LgndrUninstallGameArgs):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
yes = args.yes
get_boolean_choice = args.get_boolean_choice_handler
# def get_boolean_choice(x, default): return True
# noinspection PyShadowingBuiltins
def print(x): self.logger.info(x) if x else None

uninstaller = igame.uninstaller

print('\nThis game provides the following uninstaller:')
print(f'- {uninstaller["path"]} {uninstaller["args"]}\n')

if yes or get_boolean_choice('Do you wish to run the uninstaller?', default=True):
logger.info('Running uninstaller...')
req_path, req_exec = os.path.split(uninstaller['path'])
work_dir = os.path.join(igame.install_path, req_path)
fullpath = os.path.join(work_dir, req_exec)
try:
p = subprocess.Popen([fullpath, uninstaller['args']], cwd=work_dir, shell=True)
p.wait()
except Exception as e:
logger.error(f'Failed to run uninstaller: {e!r}')

def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
Expand Down Expand Up @@ -492,10 +558,15 @@ def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], pr
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
return len(failed), len(missing)

@unlock_installed
def import_game(self, args: LgndrImportGameArgs) -> None:
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
get_boolean_choice = args.get_boolean_choice
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return

# make sure path is absolute
args.app_path = os.path.abspath(args.app_path)
Expand Down Expand Up @@ -535,6 +606,8 @@ def import_game(self, args: LgndrImportGameArgs) -> None:
# get everything needed for import from core, then run additional checks.
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
exe_path = os.path.join(args.app_path, manifest.meta.launch_exe.lstrip('/'))
if os.name != 'nt':
exe_path = case_insensitive_file_search(exe_path)
# check if most files at least exist or if user might have specified the wrong directory
total = len(manifest.file_manifest_list.elements)
found = sum(os.path.exists(os.path.join(args.app_path, f.filename))
Expand Down Expand Up @@ -590,9 +663,18 @@ def import_game(self, args: LgndrImportGameArgs) -> None:
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
return

@unlock_installed
def egs_sync(self, args):
return super(LegendaryCLI, self).egs_sync(args)

@unlock_installed
def move(self, args):
# Override logger for the local context to use message as part of the indirect return value
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
if not self.core.lgd.lock_installed():
logger.fatal('Failed to acquire installed data lock, only one instance of Legendary may '
'install/import/move applications at a time.')
return

app_name = self._resolve_aliases(args.app_name)
igame = self.core.get_installed_game(app_name, skip_sync=True)
Expand Down
16 changes: 14 additions & 2 deletions rare/lgndr/core.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import functools
import json
import os
from multiprocessing import Queue
Expand Down Expand Up @@ -28,6 +29,15 @@ def __init__(self, override_config=None, timeout=10.0):
self.handler = LgndrCoreLogHandler()
self.log.addHandler(self.handler)

@staticmethod
def unlock_installed(func):
@functools.wraps(func)
def unlock(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
self.lgd._installed_lock.release(force=True)
return ret
return unlock

# skip_sync defaults to false but since Rare is persistent, skip by default
# def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame:
# return super(LegendaryCore, self).get_installed_game(app_name, skip_sync)
Expand All @@ -43,7 +53,7 @@ def prepare_download(self, game: Game, base_game: Game = None, base_path: str =
repair: bool = False, repair_use_latest: bool = False,
disable_delta: bool = False, override_delta_manifest: str = '',
egl_guid: str = '', preferred_cdn: str = None,
disable_https: bool = False) -> (DLManager, AnalysisResult, ManifestMeta):
disable_https: bool = False, bind_ip: str = None) -> (DLManager, AnalysisResult, ManifestMeta):
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,
Expand All @@ -56,7 +66,7 @@ def prepare_download(self, game: Game, base_game: Game = None, base_path: str =
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
disable_https=disable_https, #bind_ip=bind_ip,
)
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
# pylint: disable=E1111
Expand All @@ -76,6 +86,7 @@ def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delet
finally:
pass

@unlock_installed
def egl_import(self, app_name):
try:
super(LegendaryCore, self).egl_import(app_name)
Expand Down Expand Up @@ -121,6 +132,7 @@ def egstore_delete(self, igame: InstalledGame, delete_files=True):
if delete_files:
delete_folder(os.path.join(igame.install_path, '.egstore'))

@unlock_installed
def egl_export(self, app_name):
try:
super(LegendaryCore, self).egl_export(app_name)
Expand Down
7 changes: 6 additions & 1 deletion rare/lgndr/downloader/mp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,15 @@ def run_real(self):
self.writer_result_q = MPQueue(-1)

self.log.info(f'Starting download workers...')

bind_ip = None
for i in range(self.max_workers):
if self.bind_ips:
bind_ip = self.bind_ips[i % len(self.bind_ips)]

w = DLWorker(f'DLWorker {i + 1}', self.dl_worker_queue, self.dl_result_q,
self.shared_memory.name, logging_queue=self.logging_queue,
dl_timeout=self.dl_timeout)
dl_timeout=self.dl_timeout, bind_addr=bind_ip)
self.children.append(w)
w.start()

Expand Down
5 changes: 4 additions & 1 deletion rare/lgndr/glue/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ class LgndrImportGameArgs:
class LgndrUninstallGameArgs:
app_name: str
keep_files: bool = False
skip_uninstaller: bool = False
yes: bool = False
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = field(default_factory=LgndrIndirectStatus)
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
get_boolean_choice_main: GetBooleanChoiceProtocol = get_boolean_choice
get_boolean_choice_handler: GetBooleanChoiceProtocol = get_boolean_choice


@dataclass
Expand Down Expand Up @@ -84,6 +86,7 @@ class LgndrInstallGameArgs:
reset_sdl: bool = False
skip_sdl: bool = False
disable_https: bool = False
bind_ip: str = ""
# FIXME: move to LgndrInstallGameRealArgs
skip_dlcs: bool = False
with_dlcs: bool = False
Expand Down
4 changes: 3 additions & 1 deletion rare/shared/workers/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

logger = getLogger("UninstallWorker")


# TODO: You can use RareGame directly here once this is called inside RareCore and skip metadata fetch
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
game = core.get_game(app_name)
Expand All @@ -32,8 +33,9 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co
LgndrUninstallGameArgs(
app_name=app_name,
keep_files=keep_files,
indirect_status=status,
skip_uninstaller=False,
yes=True,
indirect_status=status,
)
)
if not keep_config:
Expand Down

0 comments on commit 821c6e1

Please sign in to comment.