diff options
Diffstat (limited to 'gamechest/gamemanager_method1.py')
-rw-r--r-- | gamechest/gamemanager_method1.py | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/gamechest/gamemanager_method1.py b/gamechest/gamemanager_method1.py new file mode 100644 index 0000000..9e2d4ac --- /dev/null +++ b/gamechest/gamemanager_method1.py @@ -0,0 +1,252 @@ +#!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 + + +class TarLzipExtractor: + + class State(Enum): + IDLE = enum_auto() + ONGOING = enum_auto() + + class Command(Enum): + STOP = enum_auto() + + def __init__(self, output_dir): + self.output_dir = output_dir + self.state_lock = threading.Lock() + self.state = self.State.IDLE + self.queue = queue.Queue(maxsize=10) + self.progress_lock = threading.Lock() + self.progress = 0 + + def _thread_worker(self, *args): + lzip_proc = tar_proc = None + try: + lzip_command = '/usr/bin/lzip' + plzip_command = '/usr/bin/plzip' + if os.path.exists(plzip_command): + lzip_command = plzip_command + lzip_proc = subprocess.Popen( + (lzip_command, '-d'), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + tar_proc = subprocess.Popen( + ('tar', '-C', self.output_dir, '-xf', '-'), + stdin=lzip_proc.stdout, + ) + filepath = f'{self.output_dir}/{self.filename}' + filesize = os.stat(filepath).st_size + progress = 0 + with open(filepath, 'rb') as fobj: + while True: + with contextlib.suppress(queue.Empty): + queue_command = self.queue.get_nowait() + if queue_command == self.Command.STOP: + return + data = fobj.read(131072) # 128kib chunks + if len(data) == 0: + lzip_proc.stdin.close() + return + lzip_proc.stdin.write(data) + with self.progress_lock: + self.progress = filesize / fobj.tell() + finally: + with self.state_lock: + self.state = self.State.IDLE + # first terminate all, then wait all, quicker than terminate/wait + # for each. + lzip_proc or lzip_proc.terminate() + tar_proc or tar_proc.terminate() + lzip_proc or lzip_proc.wait() + tar_proc or tar_proc.wait() + + def start(self, filename): + + with self.state_lock: + if self.state == self.State.ONGOING: + return + self.state = self.State.ONGOING + + self.filename = filename + + self.thread = threading.Thread( + target=self._thread_worker, + daemon=True) + self.thread.start() + + def stop(self): + + with self.state_lock: + if self.state != self.State.ONGOING: + return + self.state = self.State.IDLE + + self.queue.put(self.Command.STOP) + + def poll(self): + with self.state_lock, self.progress_lock: + return self.state, self.progress + + +class RsyncManager: + + rsync_progress_re = re.compile(r'\s(\d+)%\s') + + class State(Enum): + IDLE = enum_auto() + ONGOING = enum_auto() + + def __init__(self, repository_host, repository_path, gamedir): + self.selector = selectors.DefaultSelector() + self.repository_host = repository_host + self.repository_path = repository_path + self.gamedir = gamedir + + self.state = self.State.IDLE + self.proc = None + self.package_name = None + self.last_progress = 0 + + def start(self, package_name): + 'returns True when package_name is being installed' + + if self.state == self.State.ONGOING: + return + + self.package_name = package_name + command = ( + 'rsync', + '-a', + '--partial', + '--info=progress2', + f'{self.repository_host}:{self.repository_path}/{package_name}', + f'{self.gamedir}/.', + ) + debug('running command %s', command) + self.proc = subprocess.Popen( + command, + stdout=subprocess.PIPE, + encoding='utf8') + self.selector.register(self.proc.stdout, selectors.EVENT_READ) + self.last_progress = 0 + self.state = self.State.ONGOING + + return True + + def stop(self): + if self.state == self.State.IDLE: + return + if self.proc.poll() is None: + debug('terminating unfinished game install process for %s', + self.package_name) + self.proc.terminate() + proc_return_code = self.proc.wait() + debug('game install process return status %d', proc_return_code) + self.selector.unregister(self.proc.stdout) + self.proc = None + self.install_ongoing = False + + def poll(self): + if self.state == self.State.IDLE: + return self.state, self.last_progress + + proc = self.proc + if proc.poll() is not None: + self.stop() + self.state = self.State.IDLE + return self.state, 100 + + if not any(key + for key, mask in self.selector.select(timeout=0) + if mask & selectors.EVENT_READ and key.fileobj == proc.stdout): + return self.state, self.last_progress + + # 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 + # visible in a gui). + line = proc.stdout.readline() + if match := self.rsync_progress_re.search(line): + progress = int(match.group(1)) + self.last_progress = progress + + return self.state, self.last_progress + + +class GameArchiveRemover: + + def __init__(self, gamedir): + self.gamedir = gamedir + + def start(self, package_name): + pass + + def stop(self): + pass + + def poll(self): + pass + + +class GameInstaller: + + class State(Enum): + IDLE = enum_auto() + ONGOING = enum_auto() + + def __init__(self, repository_host, repository_path, gamedir): + self.state = self.State.IDLE + self.step = None + self.steps = ( + RsyncManager( + repository_host=repository_host, + repository_path=repository_path, + gamedir=gamedir + ), + TarLzipExtractor(output_dir=gamedir), + ) + + def start(self, package_name): + if self.state == self.State.ONGOING: + return + self.step = 0 + self.steps[self.step].start(package_name) + self.package_name = package_name + self.state = self.State.ONGOING + #self.rsync_manager.start(package_name) + #self.current_sub_process = self.rsync_manager + return True + + def stop(self): + if self.state == self.State.IDLE: + return + self.steps[self.step].stop() + #self.current_sub_process.stop() + self.state = self.State.IDLE + + def poll(self): + 'update internal state and returns current state' + if self.state == self.State.IDLE: + return self.State.IDLE, 100, 100 + sub_state, sub_progress = self.steps[self.step].poll() + if sub_state == self.steps[self.step].State.IDLE: + self.step += 1 + if self.step >= len(self.steps): + self.state = self.State.IDLE + return self.State.IDLE, 100, 100 + self.steps[self.step].start(self.package_name) + sub_progress = 0 + return self.state, (self.step/len(self.steps))*100, sub_progress |