This is an automated email from the git hooks/post-receive script. smcv pushed a commit to branch master in repository game-data-packager.
commit 4e733a9897184cbd1dfdfde3829613da1bfe325b Author: Simon McVittie <[email protected]> Date: Sun Jan 3 23:27:21 2016 +0000 runtime: add a generic Gtk launcher, initially for Unreal --- Makefile | 3 + doc/launcher.mdwn | 103 ++++++++++ runtime/confirm-binary-only.txt | 8 + runtime/launcher.py | 403 ++++++++++++++++++++++++++++++++++++++++ runtime/missing-data.txt | 4 + 5 files changed, 521 insertions(+) diff --git a/Makefile b/Makefile index a7ab191..667f29f 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,7 @@ install: default mkdir -p $(DESTDIR)/usr/share/games/game-data-packager cp -ar game_data_packager/ $(DESTDIR)/usr/share/games/game-data-packager/ + install runtime/launcher.py $(DESTDIR)/usr/share/games/game-data-packager/gdp-launcher install -m0644 out/*.copyright $(DESTDIR)/usr/share/games/game-data-packager/ install -m0644 out/*.png $(DESTDIR)/usr/share/games/game-data-packager/ install -m0644 out/*.svgz $(DESTDIR)/usr/share/games/game-data-packager/ @@ -143,6 +144,8 @@ install: default install -m0644 runtime/doom2-masterlevels.desktop $(DESTDIR)/usr/share/applications/ install -m0644 doc/doom2-masterlevels.6 $(DESTDIR)/usr/share/man/man6/ install -m0644 out/doom-common.png $(DESTDIR)/usr/share/pixmaps/doom2-masterlevels.png + install -m0644 data/confirm-binary-only.txt $(DESTDIR)/usr/share/games/game-data-packager/ + install -m0644 data/missing-data.txt $(DESTDIR)/usr/share/games/game-data-packager/ # Requires additional setup, so not part of "make check" manual-check: diff --git a/doc/launcher.mdwn b/doc/launcher.mdwn new file mode 100644 index 0000000..2c9ce87 --- /dev/null +++ b/doc/launcher.mdwn @@ -0,0 +1,103 @@ +game-data-packager's Gtk launcher stub +====================================== + +Here are some design notes about the Gtk game launcher. + +Requirements +------------ + +All of these are already implemented, and should be kept. + +* Written in a high-level language (not a shell script with Zenity or + xdialog) + - complex shell scripts are a pain to keep maintainable + +* All logic is in `game-data-packager.deb` or a future + `game-data-packager-runtime.deb`, not in the .deb that g-d-p produces + - the logic might have bugs which we want to fix + - we can rely on being able to update g-d-p itself via normal Debian + mechanisms + - we cannot rely on being able to update anything in a g-d-p-generated + .deb + +* Has a GUI + - games are often run from menu systems + +* Can check whether a representative sample of required files are present + - some games' data sets have non-obvious tangles of dependencies + +* Each user is prompted before running a binary-only game like Unreal + (or eventually Quake 4, but for now Quake 4 is handled by src:quake), + so they can choose to not run it + - this is per-user so that users can choose to run all binary-only + games as uid "jbloggs-games" or something, and protect their + (potentially root-equivalent) normal uid "jbloggs" from attacks + via compromised games + +* The launcher does not `chdir()` or manipulate `LD_LIBRARY_PATH` until the + binary-only game is actually run, and the `.desktop` file does not have + to use `WorkingDirectory=` + - again, this is to protect individual uids from being attacked via a + compromised game that they accidentally run from a menu, even if `.` + is already (unwisely) on a search path + +* Optionally constructs a symbolic link farm from one or more system-wide + search directories + - this is required for Unreal 1 + +* If constructing a symlink farm, can copy selected files instead of + linking them + - this is required for `*.ini` in Unreal 1 + +* Games can ship their own proprietary icons which will be used by our + .desktop files + - we don't have a Free reinterpretation of the logo for all games + +"Nice to have" +-------------- + +All of these are already implemented, and it would be nice to keep them. + +* Game metadata is also in `g-d-p.deb` or `g-d-p-runtime.deb`, + so we can fix its bugs + - As currently implemented, it's in an extra section in + /usr/share/games/g-d-p/unreal-gold.desktop + or similar. It isn't clear whether this is the right solution. + +* The game only installs proprietary files that we cannot fix anyway, + and symbolic links to stable paths provided by g-d-p or a separate + game engine like ioquake3 + - `/usr/games/unreal-gold` -> `/usr/share/games/g-d-p/gdp-launcher` + - `/usr/share/applications/unreal-gold.desktop` -> + `/usr/share/games/g-d-p/unreal-gold.desktop` + +* Errors while launching the game are displayed in the GUI + +* Does not import from `game_data_packager` and does not rely + on the YAML/JSON, or only relies on a defined subset (tbd) + - this means we can consider splitting out + `game-data-packager-runtime.deb` in future + +* GUI is done with a modern toolkit that supports Wayland, etc., + and has nice Python bindings + - implementation detail: it's currently Gtk 3 + +* freedesktop.org basedir compliant + - uses `XDG_DATA_HOME`, etc., for games that do not already have a + well-established dot-directory + +* Implementation does not entirely rule out being able to install game data + "for just me" while unprivileged + - this may seem rather backwards, but there's some value in having + a tool like g-d-p shared between multiple games instead of each game + inventing its own + +TODO +---- + +* Ability to make e.g. `~/.loki/ut` a symlink to e.g. `$XDG_DATA_HOME/ut99` + if the former does not already exist? + - we would need to be prepared to fall back to `~/.loki/ut` being a real + directory, though + +* Maybe supersede the ad-hoc launchers in src:quake? diff --git a/runtime/confirm-binary-only.txt b/runtime/confirm-binary-only.txt new file mode 100644 index 0000000..a17d29d --- /dev/null +++ b/runtime/confirm-binary-only.txt @@ -0,0 +1,8 @@ +${name} is a binary-only game and might contain security vulnerabilities +or other bugs. If it does, ${distro} cannot fix them. + +Using this game for multiplayer on untrusted networks is not +recommended. To protect personal files, you could create a dedicated +user ID to run games. + +This message will be shown once for each user ID that runs ${name}. diff --git a/runtime/launcher.py b/runtime/launcher.py new file mode 100755 index 0000000..c86288b --- /dev/null +++ b/runtime/launcher.py @@ -0,0 +1,403 @@ +#!/usr/bin/python3 +# encoding=utf-8 + +# game-data-packager Gtk launcher stub. See doc/launcher.mdwn for design + +# Copyright © 2015-2016 Simon McVittie <[email protected]> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# You can find the GPL license text on a Debian system under +# /usr/share/common-licenses/GPL-2. + +import argparse +import fnmatch +import logging +import os +import shlex +import shutil +import string +import sys +import traceback + +import gi +gi.require_version('Gtk', '3.0') + +from gi.repository import (GLib, GObject) +from gi.repository import Gtk + +if 'GDP_UNINSTALLED' in os.environ: + GDP_DIR = './runtime' +else: + GDP_DIR = '/usr/share/games/game-data-packager' + +GDL_GROUP = 'game-data-launcher' +GDL_KEY_BINARY_ONLY = 'BinaryOnly' +GDL_KEY_BASE_DIRECTORIES = 'BaseDirectories' +GDL_KEY_REQUIRED_FILES = 'RequiredFiles' +GDL_KEY_DOT_DIRECTORY = 'DotDirectory' +GDL_KEY_LIBRARY_PATH = 'LibraryPath' +GDL_KEY_LINK_FILES = 'LinkFiles' +GDL_KEY_COPY_FILES = 'CopyFiles' +GDL_KEY_WORKING_DIRECTORY = 'WorkingDirectory' +GDL_KEY_EXEC = 'Exec' + +# Normalize environment so we can use ${XDG_DATA_HOME} unconditionally. +# Do this before we use GLib functions that might create worker threads, +# because setenv() is not thread-safe. +ORIG_ENVIRON = os.environ.copy() +os.environ.setdefault('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) +os.environ.setdefault('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) +os.environ.setdefault('XDG_CONFIG_DIRS', '/etc/xdg') +os.environ.setdefault('XDG_DATA_HOME', os.path.expanduser('~/.local/share')) +os.environ.setdefault('XDG_DATA_DIRS', '/usr/local/share:/usr/share') + +logger = logging.getLogger('game-data-packager.launcher') +logging.basicConfig() + +if os.environ.get('GDP_DEBUG'): + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) + +DISTRO = 'the distribution' + +try: + os_release = open('/usr/lib/os-release') +except: + pass +else: + for line in os_release: + if line.startswith('NAME='): + line = line[5:].strip() + if line.startswith('"'): + line = line.strip('"') + elif line.startswith("'"): + line = line.strip("'") + DISTRO = line + +def expand(path): + if path is None: + return None + + return os.path.expanduser(os.path.expandvars(path)) + +class Launcher: + def __init__(self, argv=None): + name = os.path.basename(sys.argv[0]) + + if name.endswith('.py'): + name = name[:-3] + + parser = argparse.ArgumentParser() + parser.add_argument('--id', default=name, + help='identity of launched game (default: %s)' % name) + parser.add_argument('arguments', nargs='*', + help='arguments for the launched game') + self.args = parser.parse_args(argv) + + self.id = self.args.id + self.keyfile = GLib.KeyFile() + self.keyfile.load_from_file(os.path.join(GDP_DIR, + self.id + '.desktop'), + GLib.KeyFileFlags.NONE) + + self.name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, + GLib.KEY_FILE_DESKTOP_KEY_NAME) + logger.debug('Name: %s', self.name) + GLib.set_application_name(self.name) + + self.icon_name = self.keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP, + GLib.KEY_FILE_DESKTOP_KEY_ICON) + logger.debug('Icon: %s', self.icon_name) + + self.binary_only = self.keyfile.get_boolean(GDL_GROUP, + GDL_KEY_BINARY_ONLY) + logger.debug('Binary-only: %r', self.binary_only) + self.required_files = list(map(expand, + self.keyfile.get_string_list(GDL_GROUP, + GDL_KEY_REQUIRED_FILES))) + logger.debug('Checked files: %r', sorted(self.required_files)) + + try: + self.dot_directory = expand(self.keyfile.get_string(GDL_GROUP, + GDL_KEY_DOT_DIRECTORY)) + except: + self.dot_directory = expand('${XDG_DATA_HOME}/' + self.id) + logger.debug('Dot directory: %s', self.dot_directory) + + try: + self.base_directories = list(map(expand, + self.keyfile.get_string_list(GDL_GROUP, + GDL_KEY_BASE_DIRECTORIES))) + except: + # this launcher is for binary-only games so assume /usr/lib + self.base_directories = ['/usr/lib/' + self.id] + logger.debug('Base directories: %r', self.base_directories) + + try: + self.library_path = self.keyfile.get_string_list(GDL_GROUP, + GDL_KEY_LIBRARY_PATH) + except: + self.library_path = [] + logger.debug('Library path: %r', self.library_path) + + try: + self.working_directory = expand(self.keyfile.get_string(GDL_GROUP, + GDL_KEY_WORKING_DIRECTORY)) + except: + self.working_directory = None + logger.debug('Working directory: %s', self.working_directory) + + try: + self.link_files = self.keyfile.get_boolean(GDL_GROUP, + GDL_KEY_LINK_FILES) + except: + self.link_files = False + logger.debug('Link files: %r', self.link_files) + + if self.link_files: + try: + self.copy_files = self.keyfile.get_string_list(GDL_GROUP, + GDL_KEY_COPY_FILES) + except: + self.copy_files = [] + logger.debug('... but copy files matching: %r', self.copy_files) + else: + self.copy_files = [] + + exec_ = self.keyfile.get_string(GDL_GROUP, GDL_KEY_EXEC) + self.argv = list(map(expand, shlex.split(exec_))) + logger.debug('Arguments: %r', self.argv) + + self.exit_status = 1 + + def main(self): + have_all_data = True + warning_stamp = os.path.join(self.dot_directory, + 'confirmed-binary-only.stamp') + + for p in self.base_directories: + logger.debug('Searching: %s' % p) + + # sanity check: game engines often don't cope well with missing data + for f in self.required_files: + logger.debug('looking for %s', f) + for p in self.base_directories: + logger.debug('looking for %s in %s', f, p) + if os.path.exists(os.path.join(p, f)): + logger.debug('found %s in %s', f, p) + break + else: + logger.warning('Data file is missing: %s' % f) + have_all_data = False + + os.makedirs(self.dot_directory, exist_ok=True) + + if not have_all_data: + gui = Gui(self) + gui.text_view.get_buffer().set_text( + self.load_text('missing-data.txt', 'Data files missing')) + gui.window.show_all() + gui.check_box.hide() + Gtk.main() + sys.exit(72) # EX_OSFILE + + elif self.binary_only and not os.path.exists(warning_stamp): + self.exit_status = 77 # EX_NOPERM + gui = Gui(self) + gui.text_view.get_buffer().set_text( + self.load_text('confirm-binary-only.txt', + 'Binary-only game, we cannot fix bugs or security ' + 'vulnerabilities!')) + gui.check_box.bind_property('active', gui.ok_button, 'sensitive', + GObject.BindingFlags.SYNC_CREATE) + gui.ok_button.connect('clicked', lambda _: + self._confirm_binary_only_cb(gui)) + + gui.window.show_all() + Gtk.main() + sys.exit(self.exit_status) + + else: + try: + self.exec_game() + except: + gui = Gui(self) + gui.text_view.get_buffer().set_text(traceback.format_exc()) + gui.ok_button.set_sensitive(False) + gui.window.show_all() + gui.check_box.hide() + Gtk.main() + sys.exit(self.exit_status) + else: + raise AssertionError('exec_game should never return') + + def flush(self): + for f in (sys.stdout, sys.stderr): + f.flush() + + def _confirm_binary_only_cb(self, gui): + warning_stamp = os.path.join(self.dot_directory, + 'confirmed-binary-only.stamp') + + try: + open(warning_stamp, 'a').close() + self.exec_game() + except: + gui.text_view.get_buffer().set_text(traceback.format_exc()) + gui.check_box.hide() + gui.ok_button.set_sensitive(False) + + def exec_game(self, _unused=None): + self.exit_status = 69 # EX_UNAVAILABLE + + if self.link_files: + logger.debug('linking in files') + # prune dangling symbolic links + if os.path.exists(self.dot_directory): + logger.debug('checking %r for dangling symlinks', + self.dot_directory) + for dirpath, dirnames, filenames in os.walk(self.dot_directory): + logger.debug('walking: %r %r %r', dirpath, dirnames, + filenames) + for filename in filenames: + logger.debug('checking whether %r is a dangling ' + 'symlink', filename) + f = os.path.join(dirpath, filename) + + if not os.path.exists(f): + logger.info('Removing dangling symlink %s', f) + os.remove(f) + + logger.debug('%r', self.base_directories) + + # symlink in all base directories, highest priority first + for p in self.base_directories: + logger.debug('Searching for files to link in %s', p) + for dirpath, dirnames, filenames in os.walk(p): + logger.debug('walking: %r %r %r', dirpath, dirnames, + filenames) + for filename in filenames: + logger.debug('ensuring that %s is symlinked in', + filename) + + f = os.path.join(dirpath, filename) + logger.debug('%s', f) + assert f.startswith(p + '/') + + target = os.path.join(self.dot_directory, + f[len(p) + 1:]) + d = os.path.dirname(target) + + if os.path.exists(target): + logger.debug('Already exists: %s', target) + continue + + if os.path.lexists(target): + logger.info('Removing dangling symlink %s', target) + os.remove(target) + + if d: + logger.info('Creating directory: %s', d) + os.makedirs(d, exist_ok=True) + + for pattern in self.copy_files: + if fnmatch.fnmatch(f, pattern): + logger.info('Copying %s -> %s', f, target) + shutil.copyfile(f, target) + break + else: + logger.info('Symlinking %s -> %s', f, target) + os.symlink(f, target) + else: + logger.debug('not linking in files') + + if self.working_directory is not None: + os.chdir(self.working_directory) + + self.flush() + + environ = os.environ.copy() + + library_path = self.library_path[:] + + if 'LD_LIBRARY_PATH' in environ: + library_path.append(environ['LD_LIBRARY_PATH']) + + environ['LD_LIBRARY_PATH'] = ':'.join(library_path) + + os.execve(self.argv[0], self.argv + self.args.arguments, environ) + + raise AssertionError('nope') + raise AssertionError('os.execve should never return') + + def load_text(self, filename, placeholder): + for f in ('%s.%s' % (self.id, filename), filename): + try: + path = os.path.join(GDP_DIR, f) + text = open(path).read() + except OSError: + pass + else: + text = string.Template(text).safe_substitute( + distro=DISTRO, + name=self.name, + ) + # strip single \n + text = text.replace('\n\n', '\r\r').replace('\n', ' ') + text = text.replace('\r', '\n') + return text + else: + return placeholder + +class Gui: + def __init__(self, launcher): + self.window = Gtk.Window() + self.window.set_default_size(600, 300) + self.window.connect('delete-event', Gtk.main_quit) + self.window.set_title(launcher.name) + self.window.set_icon_name(launcher.icon_name) + + self.grid = Gtk.Grid(row_spacing=6, column_spacing=6, + margin_top=12, margin_bottom=12, margin_start=12, margin_end=12) + self.window.add(self.grid) + + image = Gtk.Image.new_from_icon_name(launcher.icon_name, + Gtk.IconSize.DIALOG) + image.set_valign(Gtk.Align.START) + self.grid.attach(image, 0, 0, 1, 1) + + self.text_view = Gtk.TextView(editable=False, cursor_visible=False, + hexpand=True, vexpand=True, wrap_mode=Gtk.WrapMode.WORD, + top_margin=6, left_margin=6, right_margin=6, bottom_margin=6) + self.grid.attach(self.text_view, 1, 0, 1, 1) + + subgrid = Gtk.Grid(column_spacing=6, column_homogeneous=True, + halign=Gtk.Align.END) + + cancel_button = Gtk.Button.new_with_label('Cancel') + cancel_button.connect('clicked', Gtk.main_quit) + subgrid.attach(cancel_button, 0, 0, 1, 1) + + self.check_box = Gtk.CheckButton.new_with_label("I'll be careful") + self.check_box.set_hexpand(True) + self.grid.attach(self.check_box, 0, 1, 2, 1) + + self.ok_button = Gtk.Button.new_with_label('Run') + self.ok_button.set_sensitive(False) + subgrid.attach(self.ok_button, 1, 0, 1, 1) + + self.grid.attach(subgrid, 0, 2, 2, 1) + + self.window.show_all() + +if __name__ == '__main__': + Launcher().main() diff --git a/runtime/missing-data.txt b/runtime/missing-data.txt new file mode 100644 index 0000000..9cc3c10 --- /dev/null +++ b/runtime/missing-data.txt @@ -0,0 +1,4 @@ +Required data files are missing. + +Please use game-data-packager to build and install the data packages +for ${name}. -- Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-games/game-data-packager.git _______________________________________________ Pkg-games-commits mailing list [email protected] http://lists.alioth.debian.org/cgi-bin/mailman/listinfo/pkg-games-commits

