summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TODO.rst20
-rwxr-xr-xgamechestcli/__main__.py3
-rw-r--r--gamechestcli/gamechest/cliactions/run.py31
-rw-r--r--gamechestcli/gamechest/gameconfig.py5
-rw-r--r--gamechestcli/gamechest/gamedb.py29
-rw-r--r--gamechestcli/gamechest/runners/extract.py140
-rw-r--r--gamechestcli/gamechest/runners/install.py2
-rw-r--r--gamechestcli/requirements.txt2
8 files changed, 204 insertions, 28 deletions
diff --git a/TODO.rst b/TODO.rst
index 6c7ca4f..93860bb 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -1,5 +1,25 @@
+- todo: quand on fait un remove, faire un remove aussi de la partie
+ ~/.cache/org.devys.org/games/<ID>
+- faire une commande reinstall qui fait juste remove puis install.
+- todo: ajouter system d'update, gérer jeux qui références une archive d'un autre jeux (subgame sortof avec parent_id: xxx, possible d'overrider la commande si précisée), avoir des alias (ex ff1 expand to final_fantasy_01), gérer jeux qui ont des options de commandes, simplifier la partie install...
+- faire une option pour reset le cache (utile pour notamment faire un
+ WINEPREFIX_RESET=1)
+- make output of list --installed consistent with --not-installed
- implem with asyncio for steppers to simplify implementation
- with cli just run asyncio.run(main())
- with gui, run gtk in a thread and asyncio.run in main thread (or the
reverse) and make them communicate with a queue.
- if not easily working, use https://blogs.gnome.org/jamesh/2019/08/05/glib-integration-for-python-asyncio/
+- sed tout les runners pour utiliser un truc genre GC_LANGUAGE (gamechest
+ language) à la place de LANGUAGE qui est une var système classique qui
+ risque de foutre la grouille sur certains jeux en la redéfinissant à des
+ valeurs qui ne sont pas standard.
+
+Whishlist cat
+=============
+- gamechest update <game>, update the game if installed
+- gamechest update, update all installed games
+- gamechest install --all, install all available games
+- gamechest remove --all, remove all available games
+- gamechest install a b c, install all these games
+
diff --git a/gamechestcli/__main__.py b/gamechestcli/__main__.py
index 73d5bab..8452b99 100755
--- a/gamechestcli/__main__.py
+++ b/gamechestcli/__main__.py
@@ -9,6 +9,9 @@ Usage: gamechest install <GAME_ID>
gamechest list [--installed|--not-installed]
gamechest showconfig
+Options:
+ --profile_id=<PROFILE_ID>, -p <PROFILE_ID>
+ use profile <PROFILE_ID> instead of the default one.
'''
import sys
diff --git a/gamechestcli/gamechest/cliactions/run.py b/gamechestcli/gamechest/cliactions/run.py
index d6791d0..1c55cc4 100644
--- a/gamechestcli/gamechest/cliactions/run.py
+++ b/gamechestcli/gamechest/cliactions/run.py
@@ -1,18 +1,47 @@
+import os
import subprocess
import sys
+from pathlib import Path
+from ..gameconfig import config
from ..gamedb import GameDB
from ..statusdb import StatusDB
def run_game(game_db, profile_id, game_info):
command = game_db.get_game_command(profile_id, game_info['id'])
+ profile_dir = Path(config.get_profile_dir(profile_id))
+ profile_game_dir = profile_dir / game_info['id']
+ game_dir = config.get_games_install_basedir() / game_info['id']
+ # note: glob('*/') globs regular files too, thus I filter on is_dir.
+ game_dir = list(item
+ for item in game_dir.glob('*') if item.is_dir())[-1]
+ game_dir = Path(game_dir)
#tools_bin_path = config.get_games_saves_tools_bin_path()
#new_env = dict(os.environ)
#new_env['PATH'] = f'{tools_bin_path}:{new_env["PATH"]}'
# path to mod/run scripts are already prepended by get_game_command, no
# need to modify the path environment variable.
- subprocess.run(command)
+ profile_lockdir = profile_dir / '.lock'
+ try:
+ profile_lockdir.mkdir(parents=True, exist_ok=False)
+ except FileExistsError:
+ print(f'ERROR: profile is already locked, {profile_lockdir}',
+ file=sys.stderr)
+ sys.exit(1)
+ try:
+ subprocess.run(
+ command,
+ env={
+ **os.environ,
+ 'PROFILE_DIR': str(profile_game_dir),
+ 'GAME_DIR': str(game_dir),
+ 'GAME_ID': game_info['id'],
+ },
+ )
+ finally:
+ profile_lockdir.rmdir()
+ print(f'INFO: {profile_lockdir} removed')
def run(game_id, profile_id):
diff --git a/gamechestcli/gamechest/gameconfig.py b/gamechestcli/gamechest/gameconfig.py
index 5e46efa..0b3d855 100644
--- a/gamechestcli/gamechest/gameconfig.py
+++ b/gamechestcli/gamechest/gameconfig.py
@@ -34,11 +34,14 @@ class GameConfig:
game_config_path.mkdir(parents=True, exist_ok=True)
game_config_filepath = game_config_path / 'config.yaml'
self.game_config_filepath = game_config_filepath
+ self.config = {}
with contextlib.ExitStack() as stack:
stack.enter_context(contextlib.suppress(FileNotFoundError))
fdin = stack.enter_context(open(game_config_filepath, 'r',
encoding='utf8'))
- self.config = yaml.safe_load(fdin)
+ # in case yaml file is empty, None will be returned, in this case
+ # fallback to empty dict.
+ self.config = yaml.safe_load(fdin) or {}
self.config = {
**DEFAULT_CONFIG_DICT,
**self.config,
diff --git a/gamechestcli/gamechest/gamedb.py b/gamechestcli/gamechest/gamedb.py
index bec90d3..1f9e8d8 100644
--- a/gamechestcli/gamechest/gamedb.py
+++ b/gamechestcli/gamechest/gamedb.py
@@ -27,24 +27,25 @@ class GameDB:
def get_game_command(self, profile_id, game_id=None):
game_info = self.get_game_info(game_id)
game_mods = game_info.get('mods', [])
- game_mods += ['locked-run-profiledir']
-
- game_dir = config.get_games_install_basedir() / game_info['id']
- # note: glob('*/') globs regular files too, thus I filter on is_dir.
- game_dir = list(item
- for item in game_dir.glob('*') if item.is_dir())[-1]
-
+ #game_mods += ['locked-run-profiledir']
+ #game_dir = config.get_games_install_basedir() / game_info['id']
+ ## note: glob('*/') globs regular files too, thus I filter on is_dir.
+ #game_dir = list(item
+ # for item in game_dir.glob('*') if item.is_dir())[-1]
tools_bin_path = config.get_games_saves_tools_bin_path()
- profile_dir = config.get_profile_dir(profile_id)
-
+ #profile_dir = config.get_profile_dir(profile_id)
+ command_from_game_info = [
+ f'{tools_bin_path}/runners/{game_info["command"][0]}',
+ *game_info['command'][1:],
+ ] if game_info.get('command') else [
+ f'{tools_bin_path}/runners/run-{game_info["id"]}',
+ ]
command = [
+ # mods
*[f'{tools_bin_path}/mod-{item}' for item in game_mods],
- profile_dir,
- game_dir,
- *[item if index > 0 else f'{tools_bin_path}/runners/{item}'
- for index, item in enumerate(game_info['command'])],
+ # runner
+ *command_from_game_info,
]
-
return command
def get_ids(self):
diff --git a/gamechestcli/gamechest/runners/extract.py b/gamechestcli/gamechest/runners/extract.py
index 6f13d89..970c956 100644
--- a/gamechestcli/gamechest/runners/extract.py
+++ b/gamechestcli/gamechest/runners/extract.py
@@ -1,7 +1,10 @@
import os
import re
+import select
import struct
import subprocess
+import threading
+import zipfile
import humanfriendly
@@ -9,13 +12,94 @@ from ..structures import Progress
from .runnerbase import RunnerBase, neutral_locale_variables
-class Extract(RunnerBase):
+class ExtractZip(RunnerBase):
+ """
+ Simple zip wrapper without multithreading. Should not be an issue as zip
+ is used for little archives only, bigger archives are tar+zstd with
+ multithreaded algorithms. So keep this class simple.
+ """
+
+ def __init__(self, src, dst):
+ self.last_progress = Progress()
+ self.read_fd, self.write_fd = os.pipe()
+ self.cancel_event = threading.Event()
+
+ # No need to lock this unless GIL is disabled.
+ self.written_bytes = 0
+ self.total_bytes = 0
+ self.progress = 0
+
+ def extract_and_report():
+ try:
+ chunk_size = 1024**2 # 1MiB chunks
+ with zipfile.ZipFile(src, 'r') as zip_ref:
+ self.total_bytes = sum(getattr(file_info, 'file_size', 0) for file_info in zip_ref.infolist() if not file_info.is_dir())
+ for file_info in zip_ref.infolist():
+ if file_info.is_dir():
+ continue
+ with zip_ref.open(file_info) as source_file:
+ target_path = os.path.join(dst, file_info.filename)
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
+ with open(target_path, 'wb') as target_file:
+ while not self.cancel_event.is_set():
+ chunk = source_file.read(chunk_size)
+ if not chunk or self.cancel_event.is_set():
+ # if we have read everything from the
+ # source file, OR, we got a cancel event
+ # in-between the read and the write, we
+ # break.
+ break
+ target_file.write(chunk)
+ self.written_bytes += len(chunk)
+ self.progress = (self.written_bytes / self.total_bytes) * 100
+ #progress_message = f"Progress: {progress:.2f}% ({bytes_written} of {total_bytes} bytes)\n"
+ os.write(self.write_fd, b'.')
+ finally:
+ self.cancel_event.set()
+ self.progress = 100
+ os.write(self.write_fd, b'.')
+ #os.close(self.read_fd)
+ #os.close(self.write_fd)
+
+ # Create a thread for extraction
+ extraction_thread = threading.Thread(target=extract_and_report)
+ extraction_thread.start()
+ self.extraction_thread = extraction_thread
+
+
+ def get_read_fd(self):
+ return self.read_fd
+
+ def progress_read(self):
+ ready, _, _ = select.select([self.read_fd], [], [])
+ if ready:
+ # flush any present data from the pipe
+ os.read(self.read_fd, 1024)
+ self.last_progress = Progress(
+ nbbytes=self.written_bytes,
+ percent=int(100 * self.written_bytes / self.total_bytes),
+ #speed=humanfriendly.parse_size(match.group(2)),
+ speed=0,
+ eta="UNK",
+ )
+ return self.last_progress
+
+ def terminate(self):
+ self.close() # same as self.close, can be called multiple time
+
+ def poll(self):
+ return None if not self.cancel_event.is_set() else 0
+
+ def close(self):
+ self.cancel_event.set()
+ self.extraction_thread.join()
+
+
+class ExtractTar(RunnerBase):
_progress_re = re.compile(r'^\s*(\S+)\s+\[([^]]+)/s\]\s+ETA\s+(\S+)\s*$')
- def __init__(self, src, dst):
- import sys
- print('src', src, 'dst', dst, file=sys.stderr)
+ def __init__(self, src, dst, compression_type):
common_parameters = dict(
encoding='utf8',
env={**os.environ,
@@ -37,13 +121,10 @@ class Extract(RunnerBase):
lzip_command = '/usr/bin/lzip'
plzip_command = '/usr/bin/plzip'
uncompress_command_to_use = [lzip_command, '--decompress']
- first_word_magic = 0
- with open(src, 'rb') as stream:
- first_word_magic = struct.unpack('>L', stream.read(4))[0]
- if first_word_magic == 0x4c5a4950: # lzip magic
+ if compression_type == 'lzip':
if os.path.exists(plzip_command):
uncompress_command_to_use = [plzip_command, '--decompress']
- elif first_word_magic == 0x28B52FFD:
+ elif compression_type == 'zstd':
uncompress_command_to_use = ['zstd', '-T0', '-d', '--stdout']
self.zip_proc = subprocess.Popen(
uncompress_command_to_use,
@@ -90,6 +171,47 @@ class Extract(RunnerBase):
self.tar_proc.wait()
+class Extract(RunnerBase):
+ def __init__(self, src, dst):
+ zip_magics = (
+ b'PK\x03\x04',
+ b'PK\x05\x06', # empty
+ b'PK\x07\x08', # spanned
+ )
+
+ first_word_magic = 0
+ first_word_magic_b = b''
+ compression_type = None
+ with open(src, 'rb') as stream:
+ first_word_magic_b = stream.read(4)
+ first_word_magic = struct.unpack('>L', first_word_magic_b)[0]
+ if first_word_magic_b in zip_magics:
+ compression_type = 'zip'
+ elif first_word_magic == 0x4c5a4950: # lzip magic
+ compression_type = 'lzip'
+ elif first_word_magic == 0x28B52FFD: # zstd magic
+ compression_type = 'zstd'
+ if compression_type == 'zip':
+ self.inner_class = ExtractZip(src, dst)
+ else:
+ self.inner_class = ExtractTar(src, dst, compression_type)
+
+ def get_read_fd(self):
+ return self.inner_class.get_read_fd()
+
+ def progress_read(self):
+ return self.inner_class.progress_read()
+
+ def terminate(self):
+ return self.inner_class.terminate()
+
+ def poll(self):
+ return self.inner_class.poll()
+
+ def close(self):
+ return self.inner_class.close()
+
+
if __name__ == '__main__':
import contextlib
import sys
diff --git a/gamechestcli/gamechest/runners/install.py b/gamechestcli/gamechest/runners/install.py
index 7b2ff7c..0b37304 100644
--- a/gamechestcli/gamechest/runners/install.py
+++ b/gamechestcli/gamechest/runners/install.py
@@ -1,5 +1,4 @@
import functools
-import os
from pathlib import Path
from .download import Download
@@ -24,7 +23,6 @@ if __name__ == '__main__':
import contextlib
import selectors
import sys
- import time
print('main test')
with contextlib.ExitStack() as stack:
stack.enter_context(contextlib.suppress(KeyboardInterrupt))
diff --git a/gamechestcli/requirements.txt b/gamechestcli/requirements.txt
index 638d583..9531801 100644
--- a/gamechestcli/requirements.txt
+++ b/gamechestcli/requirements.txt
@@ -1,4 +1,4 @@
-docopt
+docopt-ng
humanfriendly
pyyaml
rich