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