[core/lfs] Use filelock for user data

Closes #566

Co-authored-by: Mathis Dröge <mathis.droege@ewe.net>
This commit is contained in:
derrod 2023-06-17 22:58:16 +02:00
parent bdd53fb8f8
commit e26b9e60ff
5 changed files with 92 additions and 25 deletions

View file

@ -131,7 +131,8 @@ class LegendaryCore:
Handles authentication via authorization code (either retrieved manually or automatically) Handles authentication via authorization code (either retrieved manually or automatically)
""" """
try: try:
self.lgd.userdata = self.egs.start_session(authorization_code=code) with self.lgd.userdata_lock as lock:
lock.data = self.egs.start_session(authorization_code=code)
return True return True
except Exception as e: except Exception as e:
self.log.error(f'Logging in failed with {e!r}, please try again.') self.log.error(f'Logging in failed with {e!r}, please try again.')
@ -142,7 +143,8 @@ class LegendaryCore:
Handles authentication via exchange token (either retrieved manually or automatically) Handles authentication via exchange token (either retrieved manually or automatically)
""" """
try: try:
self.lgd.userdata = self.egs.start_session(exchange_token=code) with self.lgd.userdata_lock as lock:
lock.data = self.egs.start_session(exchange_token=code)
return True return True
except Exception as e: except Exception as e:
self.log.error(f'Logging in failed with {e!r}, please try again.') self.log.error(f'Logging in failed with {e!r}, please try again.')
@ -171,22 +173,23 @@ class LegendaryCore:
raise ValueError('No login session in config') raise ValueError('No login session in config')
refresh_token = re_data['Token'] refresh_token = re_data['Token']
try: try:
self.lgd.userdata = self.egs.start_session(refresh_token=refresh_token) with self.lgd.userdata_lock as lock:
lock.data = self.egs.start_session(refresh_token=refresh_token)
return True return True
except Exception as e: except Exception as e:
self.log.error(f'Logging in failed with {e!r}, please try again.') self.log.error(f'Logging in failed with {e!r}, please try again.')
return False return False
def login(self, force_refresh=False) -> bool: def _login(self, lock, force_refresh=False) -> bool:
""" """
Attempts logging in with existing credentials. Attempts logging in with existing credentials.
raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
""" """
if not self.lgd.userdata: if not lock.data:
raise ValueError('No saved credentials') raise ValueError('No saved credentials')
elif self.logged_in and self.lgd.userdata['expires_at']: elif self.logged_in and lock.data['expires_at']:
dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1]) dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
dt_now = datetime.utcnow() dt_now = datetime.utcnow()
td = dt_now - dt_exp td = dt_now - dt_exp
@ -212,8 +215,8 @@ class LegendaryCore:
except Exception as e: except Exception as e:
self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}') self.log.warning(f'Checking for EOS Overlay updates failed: {e!r}')
if self.lgd.userdata['expires_at'] and not force_refresh: if lock.data['expires_at'] and not force_refresh:
dt_exp = datetime.fromisoformat(self.lgd.userdata['expires_at'][:-1]) dt_exp = datetime.fromisoformat(lock.data['expires_at'][:-1])
dt_now = datetime.utcnow() dt_now = datetime.utcnow()
td = dt_now - dt_exp td = dt_now - dt_exp
@ -221,7 +224,7 @@ class LegendaryCore:
if dt_exp > dt_now and abs(td.total_seconds()) > 600: if dt_exp > dt_now and abs(td.total_seconds()) > 600:
self.log.info('Trying to re-use existing login session...') self.log.info('Trying to re-use existing login session...')
try: try:
self.egs.resume_session(self.lgd.userdata) self.egs.resume_session(lock.data)
self.logged_in = True self.logged_in = True
return True return True
except InvalidCredentialsError as e: except InvalidCredentialsError as e:
@ -233,7 +236,7 @@ class LegendaryCore:
try: try:
self.log.info('Logging in...') self.log.info('Logging in...')
userdata = self.egs.start_session(self.lgd.userdata['refresh_token']) userdata = self.egs.start_session(lock.data['refresh_token'])
except InvalidCredentialsError: except InvalidCredentialsError:
self.log.error('Stored credentials are no longer valid! Please login again.') self.log.error('Stored credentials are no longer valid! Please login again.')
self.lgd.invalidate_userdata() self.lgd.invalidate_userdata()
@ -242,10 +245,14 @@ class LegendaryCore:
self.log.error(f'HTTP request for login failed: {e!r}, please try again later.') self.log.error(f'HTTP request for login failed: {e!r}, please try again later.')
return False return False
self.lgd.userdata = userdata lock.data = userdata
self.logged_in = True self.logged_in = True
return True return True
def login(self, force_refresh=False) -> bool:
with self.lgd.userdata_lock as lock:
return self._login(lock, force_refresh=force_refresh)
def update_check_enabled(self): def update_check_enabled(self):
return not self.lgd.config.getboolean('Legendary', 'disable_update_check', fallback=False) return not self.lgd.config.getboolean('Legendary', 'disable_update_check', fallback=False)

View file

@ -4,11 +4,12 @@ import json
import os import os
import logging import logging
from contextlib import contextmanager
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from time import time from time import time
from .utils import clean_filename from .utils import clean_filename, LockedJSONData
from legendary.models.game import * from legendary.models.game import *
from legendary.utils.aliasing import generate_aliases from legendary.utils.aliasing import generate_aliases
@ -16,6 +17,9 @@ from legendary.models.config import LGDConf
from legendary.utils.env import is_windows_mac_or_pyi from legendary.utils.env import is_windows_mac_or_pyi
FILELOCK_DEBUG = False
class LGDLFS: class LGDLFS:
def __init__(self, config_file=None): def __init__(self, config_file=None):
self.log = logging.getLogger('LGDLFS') self.log = logging.getLogger('LGDLFS')
@ -84,6 +88,11 @@ class LGDLFS:
self.log.warning(f'Removing "{os.path.join(self.path, "manifests", "old")}" folder failed: ' self.log.warning(f'Removing "{os.path.join(self.path, "manifests", "old")}" folder failed: '
f'{e!r}, please remove manually') f'{e!r}, please remove manually')
if not FILELOCK_DEBUG:
# Prevent filelock logger from spamming Legendary debug output
filelock_logger = logging.getLogger('filelock')
filelock_logger.setLevel(logging.INFO)
# try loading config # try loading config
try: try:
self.config.read(self.config_path) self.config.read(self.config_path)
@ -130,31 +139,35 @@ class LGDLFS:
except Exception as e: except Exception as e:
self.log.debug(f'Loading aliases failed with {e!r}') self.log.debug(f'Loading aliases failed with {e!r}')
@property
@contextmanager
def userdata_lock(self) -> LockedJSONData:
"""Wrapper around the lock to automatically update user data when it is released"""
with LockedJSONData(os.path.join(self.path, 'user.json')) as lock:
try:
yield lock
finally:
self._user_data = lock.data
@property @property
def userdata(self): def userdata(self):
if self._user_data is not None: if self._user_data is not None:
return self._user_data return self._user_data
try: try:
self._user_data = json.load(open(os.path.join(self.path, 'user.json'))) with self.userdata_lock as locked:
return self._user_data return locked.data
except Exception as e: except Exception as e:
self.log.debug(f'Failed to load user data: {e!r}') self.log.debug(f'Failed to load user data: {e!r}')
return None return None
@userdata.setter @userdata.setter
def userdata(self, userdata): def userdata(self, userdata):
if userdata is None: raise NotImplementedError('The setter has been removed, use the locked userdata instead.')
raise ValueError('Userdata is none!')
self._user_data = userdata
json.dump(userdata, open(os.path.join(self.path, 'user.json'), 'w'),
indent=2, sort_keys=True)
def invalidate_userdata(self): def invalidate_userdata(self):
self._user_data = None with self.userdata_lock as lock:
if os.path.exists(os.path.join(self.path, 'user.json')): lock.clear()
os.remove(os.path.join(self.path, 'user.json'))
@property @property
def entitlements(self): def entitlements(self):

View file

@ -3,6 +3,7 @@
import os import os
import shutil import shutil
import hashlib import hashlib
import json
import logging import logging
from pathlib import Path from pathlib import Path
@ -10,6 +11,8 @@ from sys import stdout
from time import perf_counter from time import perf_counter
from typing import List, Iterator from typing import List, Iterator
from filelock import FileLock
from legendary.models.game import VerifyResult from legendary.models.game import VerifyResult
logger = logging.getLogger('LFS Utils') logger = logging.getLogger('LFS Utils')
@ -153,3 +156,45 @@ def clean_filename(filename):
def get_dir_size(path): def get_dir_size(path):
return sum(f.stat().st_size for f in Path(path).glob('**/*') if f.is_file()) return sum(f.stat().st_size for f in Path(path).glob('**/*') if f.is_file())
class LockedJSONData(FileLock):
def __init__(self, file_path: str):
super().__init__(file_path + '.lock')
self._file_path = file_path
self._data = None
self._initial_data = None
def __enter__(self):
super().__enter__()
if os.path.exists(self._file_path):
with open(self._file_path, 'r', encoding='utf-8') as f:
self._data = json.load(f)
self._initial_data = self._data
return self
def __exit__(self, exc_type, exc_val, exc_tb):
super().__exit__(exc_type, exc_val, exc_tb)
if self._data != self._initial_data:
if self._data is not None:
with open(self._file_path, 'w', encoding='utf-8') as f:
json.dump(self._data, f, indent=2, sort_keys=True)
else:
if os.path.exists(self._file_path):
os.remove(self._file_path)
@property
def data(self):
return self._data
@data.setter
def data(self, new_data):
if new_data is None:
raise ValueError('Invalid new data, use clear() explicitly to reset file data')
self._data = new_data
def clear(self):
self._data = None

View file

@ -1 +1,2 @@
requests<3.0 requests<3.0
filelock

View file

@ -37,7 +37,8 @@ setup(
install_requires=[ install_requires=[
'requests<3.0', 'requests<3.0',
'setuptools', 'setuptools',
'wheel' 'wheel',
'filelock'
], ],
extras_require=dict( extras_require=dict(
webview=['pywebview>=3.4'], webview=['pywebview>=3.4'],