diff options
Diffstat (limited to 'gamechest/gamemanager.py.backup')
-rw-r--r-- | gamechest/gamemanager.py.backup | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/gamechest/gamemanager.py.backup b/gamechest/gamemanager.py.backup new file mode 100644 index 0000000..f8f9551 --- /dev/null +++ b/gamechest/gamemanager.py.backup @@ -0,0 +1,431 @@ +#!python3 + +import contextlib +import os +import queue +import re +import selectors +import shutil +import subprocess +import tarfile +import threading +from logging import debug, info, warning, critical +from enum import Enum, auto as enum_auto + +import yaml + + +def strip_tar_ext(name): + ext = '.tar.lzip' + return name[:-len(ext)] if name.lower().endswith(ext) else name + + +class GameChestConfig: + ''' + Stores configuration for whole program run time. + + Should be pretty unmuttable. + ''' + + def __init__(self, repository_host, repository_path, gamedir, game_data): + self.repository_host = repository_host + self.repository_path = repository_path + self.gamedir = gamedir + self.game_data = game_data + + + +class LockedData: + 'every attribute access to this class is locked except lock and data' + + def __init__(self, data={}): + super().__setattr__('lock', threading.Lock()) + super().__setattr__('data', dict()) + self.data.update(data) + + def __getattr__(self, name): + with self.lock: + return self.data[name] + + def __setattr__(self, name, value): + with self.lock: + self.data[name] = value + + +class Progress(LockedData): + 'hold a progression status' + + def __init__(self): + super().__init__(self, { + 'global': 0, + 'steps_count': 1, + 'step': 0, + 'step_index': 0, + 'step_name': 'unknown', + }) + + @locked + def set_step_index(self, index): + self._step_index = index + self._global = (index/self._steps_count)*100 + + +class GameManagerBase: + ''' + Manage a worker processing steps in background and allows polling its + status (global steps progress, and current step progress). + ''' + + class State(Enum): + IDLE = enum_auto() + ONGOING = enum_auto() + + class Command(Enum): + STOP = enum_auto() + + def __init__(self, conf): + self.conf = conf + self.state_holder = LockedData() + self.state = self.State.IDLE + self.thread = None + self.lock = threading.Lock() + self.queue = queue.Queue(maxsize=10) + self.progress = Progress() + + @property + def state(self): + return self.state_holder.state + + @property.setter + def set_state(self, value): + self.state_holder.state = value + + def worker(self, *args): + self.progress.steps_count = len(self.steps) + try: + for index, step in enumerate(self.steps): + self.progress.step_index = index + for step_progress in step(): + self.progress.step = step_progress + finally: + self.state = self.State.IDLE + + def start(self, *, game_status=None): + if self.steps is None: + raise NotImplementedError + if self.state == self.State.ONGOING: + return + self.progress.global = 0 + self.progress.step = 0 + with self.lock: + self.game_status = game_status + self.thread = threading.Thread(target=self.worker, daemon=True) + self.thread.start() + self.state = self.State.ONGOING + + def stop(self): + if self.state != self.State.ONGOING: + return + self.state = self.State.IDLE + self.queue.put(self.Command.STOP) + + def poll(self): + return self.state, self.global_progress, self.step_progress + + +class GameInstaller(GameManagerBase): + ''' + Defines how to install a game: + - download it + - unarchive it + - remove the archive + ''' + + rsync_progress_re = re.compile(r'\s(\d+)%\s') + + def __init__(self, conf): + super().__init__(conf) + self.steps = ( + self._worker_rsync, + self._worker_unarchive, + self._worker_rmarchive, + ) + + def _worker_rsync_loop(self, proc, selector): + while True: + with contextlib.suppress(queue.Empty): + if self.queue.get_nowait() == self.Command.STOP: + proc.terminate() + break + + if proc.poll() is not None: + break + + if not any(key + for key, mask in selector.select(timeout=0.250) + if mask & selectors.EVENT_READ and key.fileobj == proc.stdout): + continue + + # stdout.readline is a blocking call if there is no endline in + # stdout outputed by the subprocess. + # normally rsync output should be line buffered thus at any read event + # a call to readline should not block (or not long enough to be + # able to process a queued command quick enough). + line = proc.stdout.readline() + if match := self.rsync_progress_re.search(line): + progress = int(match.group(1)) + yield progress + + def _worker_rsync(self): + package_name = self.conf.game_data['package_name'] + command = ( + 'rsync', + '-a', + '--partial', + '--info=progress2', + f'{self.conf.repository_host}:{self.conf.repository_path}/{package_name}', + f'{self.conf.gamedir}/.', + ) + debug('running command %s', command) + with contextlib.ExitStack() as stack: + proc = stack.enter_context(subprocess.Popen( + command, + stdout=subprocess.PIPE, + encoding='utf8')) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(proc.stdout, selectors.EVENT_READ) + yield from self._worker_rsync_loop(proc=proc, selector=selector) + + def _worker_unarchive(self): + with contextlib.ExitStack() as stack: + lzip_command = '/usr/bin/lzip' + plzip_command = '/usr/bin/plzip' + if os.path.exists(plzip_command): + lzip_command = plzip_command + lzip_proc = stack.enter_context(subprocess.Popen( + (lzip_command, '-d'), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + )) + tar_proc = stack.enter_context(subprocess.Popen( + ('tar', '-C', self.output_dir, '-xvf', '-'), + stdin=lzip_proc.stdout, + stdout=subprocess.PIPE, + )) + filepath = f'{self.conf.gamedir}/{self.conf.game_data["package_name"]}' + filepath_status = f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml' + fobj = stack.enter_context(open(filepath, 'rb')) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(tar_proc.stdout, selectors.EVENT_READ) + filesize = os.stat(filepath).st_size + def _itemscounter(): + while ready := selector.select(0.100): + data = tar_proc.stdout.read1() + if len(data) == 0: + break + itemscount += data.count(b'\n') + itemscount = 0 + while True: + with contextlib.suppress(queue.Empty): + if self.queue.get_nowait() == self.Command.STOP: + break + data = fobj.read(131072) # 128kib chunks + if len(data) == 0: + lzip_proc.stdin.close() + break + lzip_proc.stdin.write(data) + yield filesize / fobj.tell() + _itemscounter() + _itemscounter() + with open(filepath_status, 'wb') as fobj: + yaml.safe_dump({ + 'installed_count': itemscount, + 'installed_version': self.conf.game_data['version'], + 'installed_package_name': self.conf.game_data['package_name'], + }, fobj) + + def _worker_rmarchive(self): + os.unlink(f'{self.conf.gamedir}/{self.conf.game_data["package_name"]}') + + +class GameRemover(GameManagerBase): + ''' + Defines how to remove a game. Basically removes a directory, with progress + report. + ''' + + def __init__(self, conf): + super().__init__(conf) + self.steps = (self._worker_remove, self._worker_rm_data) + + def _worker_remove(self): + if self.game_status is None: + return + extracted_path = '/'.join(( + self.conf.gamedir, + strip_tar_ext(self.game_status['installed_package_name']), + )) + filepath_status = f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml' + with open(filepath_status) as stream: + pkg_status = yaml.safe_load(stream) + with contextlib.ExitStack() as stack: + proc = stack.enter_context(subprocess.Popen( + ('rm', '-rvf', '--', extracted_path), + stdout=subprocess.PIPE, + )) + selector = stack.enter_context(selectors.DefaultSelector()) + selector.register(proc.stdout, selectors.EVENT_READ) + itemscount = 0 + while True: + with contextlib.suppress(queue.Empty): + if self.queue.get_nowait() == self.Command.STOP: + proc.terminate() + break + if ready := selector.select(0.250): + data = proc.stdout.read1() + if len(data) == 0: + break + itemscount += data.count(b'\n') + yield itemscount*100/pkg_status['installed_count'] + + def _worker_rm_data(self): + if self.game_status is None: + return + os.unlink(f'{self.conf.gamedir}/{self.conf.game_data["name"]}.yaml') + + +class GameUpdater(GameManagerBase): + ''' + Combine GameRemover and GameInstaller to form a consistent updating unit. + ''' + + def __init__(self, conf): + super().__init__(conf) + self._remover = GameRemover(conf) + self._installer = GameInstaller(conf) + self.steps = self._remover.steps + self._installer.steps + + + def _worker(self): + + +#class GameData: +# +# class + + +class GameManager: + + class State(Enum): + IDLE = enum_auto() + INSTALLING = enum_auto() + REMOVING = enum_auto() + UPDATING_REMOVING = enum_auto() + UPDATING_INSTALLING = enum_auto() + + def __init__(self, games_data_yaml_path, games_dir): + self.games_data_yaml_path = games_data_yaml_path + self.games_dir = games_dir + self.state = self.State.IDLE + self.update_state = self.State.IDLE + self.current_game_name = None + self.current_game_item = None + self.current_game_status = {} + + with open(games_data_yaml_path) as stream: + self.games_data = yaml.safe_load(stream) + + self.game_installer = GameInstaller( + self.games_data['repository']['host'], + self.games_data['repository']['path'], + self.games_dir, + ) + self.game_remover = GameRemover( + self.games_data['repository']['host'], + self.games_data['repository']['path'], + self.games_dir, + ) + + def get_game_item(self, name): + self.current_game_item = next( + item + for item in self.games_data['games'] + if item['name'] == name + ) + return self.current_game_item + + def get_game_status(self, name): + self.current_game_status = {} + with contextlib.suppress(FileNotFoundError): + with open(f'{self.conf.gamedir}/{name}.yaml') as stream: + self.current_game_status = yaml.safe_load(stream) + return self.current_game_status + + def is_game_updatable(self, name): + #self. ### FIXME: Stopped here ? + if not self.current_game_name: + return False + ivers = self.current_game_status.get('installed_version', None) + # only call "updatable" if there was at least a version installed + # else, this is just an "installable" package. + if ivers is not None and ivers < self.current_game_item['version']: + return True + return False + + def set_current_game(self, name): + if self.current_game_name == name: + return + self.current_game_name = name + self.get_game_item(name) + self.get_game_status(name) + + def start_install(self, name): + if self.state != self.State.IDLE: + return + self.status = self.State.INSTALLING + self.set_current_game(name) + self.game_installer.start( + self.current_game_item, + game_status=self.current_game_status, + ) + + def start_remove(self, name): + if self.state != self.State.IDLE: + return + self.status = self.State.REMOVING + self.set_current_game(name) + self.game_remover.start( + self.current_game_item, + game_status=self.current_game_status, + ) + + def start_update(self, name): + if self.state != self.State.IDLE: + return + self.status = self.State.UPDATING_REMOVING + self.set_current_game(name) + self.game_remover.start( + self.current_game_item, + game_status=self.current_game_status, + ) + + def stop(self): + self.state = self.State.IDLE + self.game_remover.stop() + self.game_installer.stop() + + def poll(self): + entry_state = self.state + return_value = { + self.State.IDLE: (lambda: GameManagerBase.State.IDLE, 0, 0), + self.State.INSTALLING: self.game_installer.poll, + self.State.REMOVING: self.game_remover.poll, + self.State.UPDATING_REMOVING: self.game_remover.poll, + self.State.UPDATING_INSTALLING: self.game_installer.poll, + }[entry_state]() + if return_value[0] == GameManagerBase.Sate.IDLE: + self.state = self.State.IDLE + if entry_state == self.State.UPDATING_REMOVING: + self.start_install(self.current_game_name) + return_value = self.poll() + return self.state, return_value[1], return_value[2] |