mirror of
https://github.com/derrod/legendary.git
synced 2024-12-22 01:45:28 +00:00
[cli/core/lfs] Add slightly janky lock for installed game data
In order to prevent multiple instances of Legendary mucking with installed game data acquire a lock as soon as it is required and only release it (implicitly) when Legendary exits. This is a bit jank, but should prevent people from messing up their local data by running two install commands at a time. EGL sync is technically also affected by this, but in its case we simply skip the sync/import/export and leave it to the next instance with a lock to do.
This commit is contained in:
parent
e26b9e60ff
commit
4145381b93
|
@ -315,7 +315,7 @@ class LegendaryCLI:
|
|||
|
||||
print('\nInstalled games:')
|
||||
for game in games:
|
||||
if game.install_size == 0:
|
||||
if game.install_size == 0 and self.core.lgd.lock_installed():
|
||||
logger.debug(f'Updating missing size for {game.app_name}')
|
||||
m = self.core.load_manifest(self.core.get_installed_manifest(game.app_name)[0])
|
||||
game.install_size = sum(fm.file_size for fm in m.file_manifest_list.elements)
|
||||
|
@ -472,12 +472,15 @@ class LegendaryCLI:
|
|||
logger.info(f'Checking "{igame.title}" ({igame.app_name})')
|
||||
# override save path only if app name is specified
|
||||
if args.app_name and args.save_path:
|
||||
if not self.core.lgd.lock_installed():
|
||||
logger.error('Unable to lock install data, cannot modify save path.')
|
||||
break
|
||||
logger.info(f'Overriding save path with "{args.save_path}"...')
|
||||
igame.save_path = args.save_path
|
||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||
|
||||
# if there is no saved save path, try to get one
|
||||
if not igame.save_path:
|
||||
# if there is no saved save path, try to get one, skip if we cannot get a install data lock
|
||||
if not igame.save_path and self.core.lgd.lock_installed():
|
||||
if args.yes and not args.accept_path:
|
||||
logger.info('Save path for this title has not been set, skipping due to --yes')
|
||||
continue
|
||||
|
@ -796,6 +799,11 @@ class LegendaryCLI:
|
|||
subprocess.Popen(command, env=full_env)
|
||||
|
||||
def install_game(self, args):
|
||||
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):
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
|
@ -1129,6 +1137,11 @@ class LegendaryCLI:
|
|||
logger.info('Automatic installation not available on Linux.')
|
||||
|
||||
def uninstall_game(self, args):
|
||||
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)
|
||||
if not igame:
|
||||
|
@ -1259,6 +1272,11 @@ class LegendaryCLI:
|
|||
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
||||
|
||||
def import_game(self, args):
|
||||
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)
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
|
@ -1354,6 +1372,11 @@ class LegendaryCLI:
|
|||
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||
|
||||
def egs_sync(self, args):
|
||||
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
|
||||
|
||||
if args.unlink:
|
||||
logger.info('Unlinking and resetting EGS and LGD sync...')
|
||||
self.core.lgd.config.remove_option('Legendary', 'egl_programdata')
|
||||
|
@ -2509,6 +2532,11 @@ class LegendaryCLI:
|
|||
logger.info('Saved choices to configuration.')
|
||||
|
||||
def move(self, args):
|
||||
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)
|
||||
if not igame:
|
||||
|
|
|
@ -1749,6 +1749,9 @@ class LegendaryCore:
|
|||
def egl_import(self, app_name):
|
||||
if not self.asset_valid(app_name):
|
||||
raise ValueError(f'To-be-imported game {app_name} not in game asset database!')
|
||||
if not self.lgd.lock_installed():
|
||||
self.log.warning('Could not acquire lock for EGL import')
|
||||
return
|
||||
|
||||
self.log.debug(f'Importing "{app_name}" from EGL')
|
||||
# load egl json file
|
||||
|
@ -1796,9 +1799,12 @@ class LegendaryCore:
|
|||
|
||||
# mark game as installed
|
||||
_ = self._install_game(lgd_igame)
|
||||
return
|
||||
|
||||
def egl_export(self, app_name):
|
||||
if not self.lgd.lock_installed():
|
||||
self.log.warning('Could not acquire lock for EGL import')
|
||||
return
|
||||
|
||||
self.log.debug(f'Exporting "{app_name}" to EGL')
|
||||
# load igame/game
|
||||
lgd_game = self.get_game(app_name)
|
||||
|
@ -1860,6 +1866,10 @@ class LegendaryCore:
|
|||
"""
|
||||
Sync game installs between Legendary and the Epic Games Launcher
|
||||
"""
|
||||
if not self.lgd.lock_installed():
|
||||
self.log.warning('Could not acquire lock for EGL sync')
|
||||
return
|
||||
|
||||
# read egl json files
|
||||
if app_name:
|
||||
lgd_igame = self._get_installed_game(app_name)
|
||||
|
|
|
@ -9,6 +9,8 @@ from collections import defaultdict
|
|||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
from .utils import clean_filename, LockedJSONData
|
||||
|
||||
from legendary.models.game import *
|
||||
|
@ -114,6 +116,8 @@ class LGDLFS:
|
|||
self.config.set('Legendary', '; Disables the notice about an available update on exit')
|
||||
self.config.set('Legendary', 'disable_update_notice', 'false' if is_windows_mac_or_pyi() else 'true')
|
||||
|
||||
self._installed_lock = FileLock(os.path.join(self.path, 'installed.json') + '.lock')
|
||||
|
||||
try:
|
||||
self._installed = json.load(open(os.path.join(self.path, 'installed.json')))
|
||||
except Exception as e:
|
||||
|
@ -293,6 +297,27 @@ class LGDLFS:
|
|||
except Exception as e:
|
||||
self.log.warning(f'Failed to delete file "{f}": {e!r}')
|
||||
|
||||
def lock_installed(self) -> bool:
|
||||
"""
|
||||
Locks the install data. We do not care about releasing this lock.
|
||||
If it is acquired by a Legendary instance it should own the lock until it exits.
|
||||
Some operations such as egl sync may be simply skipped if a lock cannot be acquired
|
||||
"""
|
||||
if self._installed_lock.is_locked:
|
||||
return True
|
||||
|
||||
try:
|
||||
self._installed_lock.acquire(blocking=False)
|
||||
# reload data in case it has been updated elsewhere
|
||||
try:
|
||||
self._installed = json.load(open(os.path.join(self.path, 'installed.json')))
|
||||
except Exception as e:
|
||||
self.log.debug(f'Failed to load installed game data: {e!r}')
|
||||
|
||||
return True
|
||||
except TimeoutError:
|
||||
return False
|
||||
|
||||
def get_installed_game(self, app_name):
|
||||
if self._installed is None:
|
||||
try:
|
||||
|
|
Loading…
Reference in a new issue