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:')
|
print('\nInstalled games:')
|
||||||
for game in 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}')
|
logger.debug(f'Updating missing size for {game.app_name}')
|
||||||
m = self.core.load_manifest(self.core.get_installed_manifest(game.app_name)[0])
|
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)
|
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})')
|
logger.info(f'Checking "{igame.title}" ({igame.app_name})')
|
||||||
# override save path only if app name is specified
|
# override save path only if app name is specified
|
||||||
if args.app_name and args.save_path:
|
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}"...')
|
logger.info(f'Overriding save path with "{args.save_path}"...')
|
||||||
igame.save_path = args.save_path
|
igame.save_path = args.save_path
|
||||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||||
|
|
||||||
# if there is no saved save path, try to get one
|
# 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:
|
if not igame.save_path and self.core.lgd.lock_installed():
|
||||||
if args.yes and not args.accept_path:
|
if args.yes and not args.accept_path:
|
||||||
logger.info('Save path for this title has not been set, skipping due to --yes')
|
logger.info('Save path for this title has not been set, skipping due to --yes')
|
||||||
continue
|
continue
|
||||||
|
@ -796,6 +799,11 @@ class LegendaryCLI:
|
||||||
subprocess.Popen(command, env=full_env)
|
subprocess.Popen(command, env=full_env)
|
||||||
|
|
||||||
def install_game(self, args):
|
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)
|
args.app_name = self._resolve_aliases(args.app_name)
|
||||||
if self.core.is_installed(args.app_name):
|
if self.core.is_installed(args.app_name):
|
||||||
igame = self.core.get_installed_game(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.')
|
logger.info('Automatic installation not available on Linux.')
|
||||||
|
|
||||||
def uninstall_game(self, args):
|
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)
|
args.app_name = self._resolve_aliases(args.app_name)
|
||||||
igame = self.core.get_installed_game(args.app_name)
|
igame = self.core.get_installed_game(args.app_name)
|
||||||
if not igame:
|
if not igame:
|
||||||
|
@ -1259,6 +1272,11 @@ class LegendaryCLI:
|
||||||
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
||||||
|
|
||||||
def import_game(self, args):
|
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
|
# make sure path is absolute
|
||||||
args.app_path = os.path.abspath(args.app_path)
|
args.app_path = os.path.abspath(args.app_path)
|
||||||
args.app_name = self._resolve_aliases(args.app_name)
|
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.')
|
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||||
|
|
||||||
def egs_sync(self, args):
|
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:
|
if args.unlink:
|
||||||
logger.info('Unlinking and resetting EGS and LGD sync...')
|
logger.info('Unlinking and resetting EGS and LGD sync...')
|
||||||
self.core.lgd.config.remove_option('Legendary', 'egl_programdata')
|
self.core.lgd.config.remove_option('Legendary', 'egl_programdata')
|
||||||
|
@ -2509,6 +2532,11 @@ class LegendaryCLI:
|
||||||
logger.info('Saved choices to configuration.')
|
logger.info('Saved choices to configuration.')
|
||||||
|
|
||||||
def move(self, args):
|
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)
|
app_name = self._resolve_aliases(args.app_name)
|
||||||
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
||||||
if not igame:
|
if not igame:
|
||||||
|
|
|
@ -1749,6 +1749,9 @@ class LegendaryCore:
|
||||||
def egl_import(self, app_name):
|
def egl_import(self, app_name):
|
||||||
if not self.asset_valid(app_name):
|
if not self.asset_valid(app_name):
|
||||||
raise ValueError(f'To-be-imported game {app_name} not in game asset database!')
|
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')
|
self.log.debug(f'Importing "{app_name}" from EGL')
|
||||||
# load egl json file
|
# load egl json file
|
||||||
|
@ -1796,9 +1799,12 @@ class LegendaryCore:
|
||||||
|
|
||||||
# mark game as installed
|
# mark game as installed
|
||||||
_ = self._install_game(lgd_igame)
|
_ = self._install_game(lgd_igame)
|
||||||
return
|
|
||||||
|
|
||||||
def egl_export(self, app_name):
|
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')
|
self.log.debug(f'Exporting "{app_name}" to EGL')
|
||||||
# load igame/game
|
# load igame/game
|
||||||
lgd_game = self.get_game(app_name)
|
lgd_game = self.get_game(app_name)
|
||||||
|
@ -1860,6 +1866,10 @@ class LegendaryCore:
|
||||||
"""
|
"""
|
||||||
Sync game installs between Legendary and the Epic Games Launcher
|
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
|
# read egl json files
|
||||||
if app_name:
|
if app_name:
|
||||||
lgd_igame = self._get_installed_game(app_name)
|
lgd_igame = self._get_installed_game(app_name)
|
||||||
|
|
|
@ -9,6 +9,8 @@ from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
from filelock import FileLock
|
||||||
|
|
||||||
from .utils import clean_filename, LockedJSONData
|
from .utils import clean_filename, LockedJSONData
|
||||||
|
|
||||||
from legendary.models.game import *
|
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', '; 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.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:
|
try:
|
||||||
self._installed = json.load(open(os.path.join(self.path, 'installed.json')))
|
self._installed = json.load(open(os.path.join(self.path, 'installed.json')))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -293,6 +297,27 @@ class LGDLFS:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.warning(f'Failed to delete file "{f}": {e!r}')
|
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):
|
def get_installed_game(self, app_name):
|
||||||
if self._installed is None:
|
if self._installed is None:
|
||||||
try:
|
try:
|
||||||
|
|
Loading…
Reference in a new issue