summaryrefslogtreecommitdiffstats
path: root/gamechest/gamemanager.py.backup
diff options
context:
space:
mode:
Diffstat (limited to 'gamechest/gamemanager.py.backup')
-rw-r--r--gamechest/gamemanager.py.backup431
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]