[game-data-packager] 06/09: runtime: add a generic Gtk launcher, initially for Unreal
Simon McVittie
smcv at debian.org
Mon Jan 4 09:04:50 UTC 2016
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 <smcv at debian.org>
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 <smcv at debian.org>
+#
+# 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
More information about the Pkg-games-commits
mailing list