Installer rewriting almost complete + Gio mounts handled in Downloader

This commit is contained in:
Mathieu Comandon 2013-05-26 18:43:42 +02:00
parent 14a6881627
commit b3a85220cb
9 changed files with 327 additions and 464 deletions

View file

@ -46,8 +46,9 @@ except ImportError:
from lutris.constants import CONFIG_EXTENSION, GAME_CONFIG_PATH
from lutris.util.log import logger
from lutris.installer import Installer
from lutris.installer import InstallerDialog
from lutris.config import check_config
from lutris.game import LutrisGame
from lutris.pga import get_games
from lutris.gui.lutriswindow import LutrisWindow
@ -62,11 +63,7 @@ parser.add_option("-i", "--install", dest="installer_file",
help="Install a game from a yml file")
parser.add_option("-l", "--list-games", action="store_true",
help="List all games in database")
parser.add_option("-f", "--force-install", action="store_true",
help="Force installation even if game is installed")
parser.add_option("-n", "--no-download",
action="store_true", dest="no_download",
help="Don't download anything when installing")
parser.add_option("--reinstall", action="store_true", help="Reinstall game")
(options, args) = parser.parse_args()
# Set the logging level to show debug messages.
@ -91,38 +88,24 @@ check_config(force_wipe=False)
installer = False
game = None
if options.installer_file:
if not os.path.exists(options.installer_file):
logger.error("Unable to find installer at : %s",
options.installer_file)
exit()
else:
game, _ext = os.path.splitext(options.installer_file)
installer = os.path.abspath(options.installer_file)
logger.info("Installing game from %s" % installer)
GObject.threads_init()
# Run the application.
signal.signal(signal.SIGINT, signal.SIG_DFL)
GObject.threads_init()
game_slug = []
for arg in args:
if arg.startswith('lutris:'):
game = arg[7:]
game_slug = arg[7:]
break
if game:
file_path = os.path.join(GAME_CONFIG_PATH, game + CONFIG_EXTENSION)
if os.path.exists(file_path) and not options.force_install:
import lutris.game
logger.info('Launching ' + game)
lutris_game = lutris.game.LutrisGame(game)
if game_slug or options.installer_file:
file_path = os.path.join(GAME_CONFIG_PATH, game_slug + CONFIG_EXTENSION)
if os.path.exists(file_path) and not options.reinstall:
lutris_game = LutrisGame(game_slug)
lutris_game.play()
else:
logger.debug('Installing %s' % game)
installer = Installer(game, installer)
InstallerDialog(options.installer_file or game_slug)
Gtk.main()
else:
lutris_window = LutrisWindow()
GObject.threads_init()
LutrisWindow()
Gtk.main()
logger.debug("Application exit")

View file

@ -6,9 +6,9 @@ Writing installers
Displaying an 'Insert disc' dialog
----------------------------------
The ``insert-disc`` command will display a message box to the user requesting
The ``insert-disc`` command will display a message box to the user requesting
him to insert the game's disc into the optical drive. A link to CDEmu homepage's
and PPA will also be displayed if the program isn't detected on the machine,
and PPA will also be displayed if the program isn't detected on the machine,
otherwise it will be replaced with a button to open gCDEmu.
An optional parameter ``message`` will override the default text if given.
@ -17,16 +17,24 @@ An optional parameter ``message`` will override the default text if given.
Moving files
------------
Move files by using the ``move`` command. ``move`` requires two parameters:
Move files by using the ``move`` command. ``move`` requires two parameters:
``src`` and ``dst``.
The ``src`` parameter can either be a ``file id`` or a relative location. If the
parameter value is not found in the list of ``file ids``, then it must be
parameter value is not found in the list of ``file ids``, then it must be
prefixed by either ``$CACHE`` or ``$GAMEDIR`` to move a file or directory from
the download cache or the game installation dir, respectively.
The ``dst`` parameter should be prefixed by either ``$GAMEDIR`` or ``$HOME``
to move files to path relative to the game dir or the current user's home
The ``dst`` parameter should be prefixed by either ``$GAMEDIR`` or ``$HOME``
to move files to path relative to the game dir or the current user's home
directory.
The ``move`` command cannot overwrite files.
Calling the installer
=====================
The installer can be called with the ``lutris:<game-slug>`` url scheme or by
specifying the path to an installer script.

View file

@ -1,6 +1,6 @@
""" Non-blocking Gio Downloader """
import time
from gi.repository import Gio, GLib, GObject
from gi.repository import Gio, GLib, Gtk
class Downloader():
@ -14,7 +14,6 @@ class Downloader():
def __init__(self, url, dest):
self.remote = Gio.File.new_for_uri(url)
self.local = Gio.File.new_for_path(dest)
self.job_cancellable = Gio.Cancellable()
self.cancellable = Gio.Cancellable()
self.progress = 0
self.start_time = None
@ -29,22 +28,44 @@ class Downloader():
def cancel(self):
self.cancellable.cancel()
self.job_cancellable.cancel()
self.cancelled = True
def download(self, job, cancellable, user_data):
def download(self, job, cancellable, _data):
flags = Gio.FileCopyFlags.OVERWRITE
try:
self.remote.copy(self.local, flags, self.cancellable,
self.progress_callback, None)
except GLib.GError:
print "Download canceled"
self.cancelled = True
self.progress_callback, None)
except GLib.GError as ex:
print "transfer error:", ex.message
if ex.code == Gio.IOErrorEnum.TIMED_OUT:
# For unknown reasons, FTP transfers times out at 25 seconds
# Hint: 25 seconds is the default timeout of GDusProxy
# https://developer.gnome.org/gio/2.26/GDBusProxy.html#GDBusProxy--g-default-timeout
print "FTP tranfers not supported yet"
def schedule_download(self, data):
def mount_cb(self, fileobj, result, _data):
try:
mount_success = fileobj.mount_enclosing_volume_finish(result)
if mount_success:
GLib.idle_add(self.schedule_download)
except GLib.GError as ex:
if(ex.code != Gio.IOErrorEnum.ALREADY_MOUNTED and
ex.code != Gio.IOErrorEnum.NOT_SUPPORTED):
print ex.message
def schedule_download(self):
Gio.io_scheduler_push_job(self.download, None,
GLib.PRIORITY_HIGH, self.job_cancellable)
GLib.PRIORITY_DEFAULT_IDLE,
Gio.Cancellable())
def start(self):
self.start_time = time.time()
GLib.idle_add(self.schedule_download, None)
if not self.remote.query_exists(Gio.Cancellable()):
self.remote.mount_enclosing_volume(Gio.MountMountFlags.NONE,
Gtk.MountOperation(),
Gio.Cancellable(),
self.mount_cb,
None)
else:
GLib.idle_add(self.schedule_download)

View file

@ -97,10 +97,10 @@ class DownloadDialog(Gtk.Dialog):
def __init__(self, url, dest):
super(DownloadDialog, self).__init__("Downloading file")
self.set_size_request(560, 100)
#self.connect('destroy', self.destroy_cb)
params = {'url': url, 'dest': dest}
self.download_progress_box = DownloadProgressBox(params)
self.download_progress_box.connect('complete', self.download_complete)
self.download_progress_box.connect('cancelrequested', self.download_cancelled)
label = Gtk.Label(label='Downloading %s' % url)
label.set_selectable(True)
label.set_padding(0, 0)
@ -110,17 +110,11 @@ class DownloadDialog(Gtk.Dialog):
self.show_all()
self.download_progress_box.start()
def destroy_cb(self, widget, data=None):
"""Action triggered when window is closed"""
self.destroy()
def download_complete(self, _widget, _data):
self.destroy()
def download_cancel(self, _widget, _data=None):
"""Action triggered when download is cancelled"""
#self.download_progress_box.cancel()
pass
def download_cancelled(self, _widget, data):
self.destroy()
class PgaSourceDialog(GtkBuilderDialog):

View file

@ -420,6 +420,7 @@ class DownloadProgressBox(Gtk.HBox):
if self.downloader.cancelled:
self.progressbar.set_fraction(0)
self.progress_label.set_text("Download canceled")
self.emit('cancelrequested', {})
return False
self.progressbar.set_fraction(progress)
megabytes = 1024 * 1024

View file

@ -1,25 +1,8 @@
#!/usr/bin/python
# -*- coding:Utf-8 -*-
#
# Copyright (C) 2010 Mathieu Comandon <strider@strycore.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# 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. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# pylint: disable=E1101, E0611
"""Installer module"""
import os
import yaml
import shutil
import urllib
import urllib2
import platform
import subprocess
@ -30,56 +13,205 @@ from os.path import join, exists
from lutris import pga
from lutris.util.log import logger
from lutris.util import http
from lutris.util.strings import slugify
from lutris.util.files import calculate_md5
from lutris.util import extract
from lutris.game import LutrisGame
#from lutris.config import LutrisConfig
from lutris.gui.dialogs import ErrorDialog, FileDialog
from lutris.gui.dialogs import FileDialog
from lutris.gui.widgets import DownloadProgressBox, FileChooserEntry
from lutris.shortcuts import create_launcher
from lutris import settings
from lutris.runners import import_task
def run_installer(filename):
"""Run an installer of .sh or .run type"""
subprocess.call(["chmod", "+x", filename])
subprocess.call([filename])
def reporthook(piece, received_bytes, total_size):
"""Follows the progress of a download"""
print("%d %%" % ((piece * received_bytes) * 100 / total_size))
class ScriptingError(Exception):
""" Custom exception for scripting errors, can be caught by modifying
excepthook """
def __init__(self, message, faulty_data=None):
self.message = message
self.faulty_data = faulty_data
logger.error(self.message + repr(self.faulty_data))
super(ScriptingError, self).__init__()
def __str__(self):
return self.message + "\n" + repr(self.faulty_data)
class ScriptInterpreter(object):
""" Class that converts raw script data to actions """
game_name = None
game_slug = None
game_files = {}
target_path = None
errors = []
def __init__(self, script):
self.script = yaml.safe_load(script)
def __init__(self, game_ref, parent):
self.parent = parent
self.script = self._fetch_script(game_ref)
if not self.is_valid():
raise ScriptingError("Invalid script", self.script)
self.game_name = self.script.get('name')
self.game_slug = slugify(self.game_name)
self.target_path = self.default_target
@property
def default_target(self):
#games_dir = self.lutris_config.get_path()
return join(os.path.expanduser('~'), self.game_slug)
def _fetch_script(self, game_ref):
if os.path.exists(game_ref):
script_contents = open(game_ref, 'r').read()
else:
full_url = settings.INSTALLER_URL + game_ref + '.yml'
request = urllib2.Request(url=full_url)
try:
request = urllib2.urlopen(request)
script_contents = request.read()
except IOError:
raise ScriptingError("Server unreachable", full_url)
return yaml.safe_load(script_contents)
def is_valid(self):
""" Return True if script is usable """
required_fields = ('runner', 'name', 'installer')
for field in required_fields:
if not self.script.get(field):
self.errors.append("Missing field '%s'" % field)
return not bool(self.errors)
def run(self):
""" Launch the install process """
if not os.path.exists(self.target_path):
os.makedirs(self.target_path)
else:
raise ScriptingError("Target path already exists",
self.target_path)
self.iter_game_files()
def iter_game_files(self):
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
if not exists(dest_dir):
logger.debug('Creating destination directory %s' % dest_dir)
os.mkdir(dest_dir)
files = self.script.get('files', [])
if len(self.game_files) < len(files):
logger.info(
"Downloading file %d of %d",
len(self.game_files) + 1, len(self.script["files"])
)
self._download_file(self.script["files"][len(self.game_files)])
else:
self._iter_commands()
def _download_file(self, game_file):
"""Download a file referenced in the installer script
Game files can be either a string, containing the location of the
file to fetch or a dict with the following keys:
- url : location of file, if not present, filename will be used
this should be the case for local files
- filename : force destination filename when url is present or path
of local file
"""
# Setup file_id, file_uri and local filename
file_id = game_file.keys()[0]
if isinstance(game_file[file_id], dict):
filename = game_file[file_id]['filename']
file_uri = game_file[file_id]['url']
if file_uri.startswith("/"):
file_uri = "file://" + file_uri
else:
file_uri = game_file[file_id]
filename = os.path.basename(file_uri)
logger.debug("Fetching [%s]: %s" % (file_id, file_uri))
# Check for file availability in PGA
pga_uri = pga.check_for_file(self.game_slug, file_id)
if pga_uri:
file_uri = pga_uri
# Setup destination path
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
dest_file = os.path.join(dest_dir, filename)
if os.path.exists(dest_file):
logger.debug("Destination file exists")
if settings.KEEP_CACHED_ASSETS:
# Fast !
self.game_files[file_id] = dest_file
self.iter_game_files()
return
else:
os.remove(dest_file)
if file_uri == "N/A":
#Ask the user where is located the file
file_uri = self.parent.ask_user_for_file()
# Change parent's status
self.parent.set_status('Fetching %s' % file_uri)
self.game_files[file_id] = dest_file
self.parent.start_download(file_uri, dest_file)
def _iter_commands(self):
os.chdir(self.target_path)
for command in self.script['installer']:
method, params = self._map_command(command)
method(self, params)
self.parent.set_status("Writing configuration")
self._write_config()
self._cleanup()
self.parent.set_status("Installation finished !")
self.parent.on_install_finished()
def _cleanup(self):
print "To delete:"
for game_file in self.game_files:
print game_file
def _write_config(self):
"""Write the game configuration as a Lutris launcher."""
config_filename = join(settings.CONFIG_DIR,
"games/%s.yml" % self.game_slug)
config_data = {
'game': {},
'realname': self.script['name'],
'runner': self.script['runner']
}
is_64bit = platform.machine() == "x86_64"
exe = 'exe64' if 'exe64' in self.script and is_64bit else 'exe'
for launcher in [exe, 'iso', 'rom', 'disk', 'main_file']:
if launcher in self.script:
if launcher == "exe64":
key = "exe"
else:
key = launcher
game_resource = self.script[launcher]
if type(game_resource) == list:
resource_paths = []
for res in game_resource:
if res in self.game_files:
resource_paths.append(self.game_files[res])
else:
resource_paths.append(res)
config_data['game'][key] = resource_paths
else:
if game_resource in self.game_files:
game_resource = self.game_files[game_resource]
else:
game_resource = join(self.target_path, game_resource)
config_data['game'][key] = game_resource
yaml_config = yaml.safe_dump(config_data, default_flow_style=False)
with open(config_filename, "w") as config_file:
config_file.write(yaml_config)
@classmethod
def _map_command(cls, command_data):
""" Converts a line from the installer directive an internal method """
if isinstance(command_data, dict):
command_name = command_data.keys()[0]
command_params = command_data[command_name]
@ -94,19 +226,18 @@ class ScriptInterpreter(object):
return getattr(cls, command_name), command_params
def _substitute(self, path_ref, path_type):
""" Replace path aliases with real paths """
if not path_ref.startswith("$%s" % path_type):
return
if path_type == "GAMEDIR":
if not self.gamedir:
raise ValueError("No gamedir set")
else:
return path_ref.replace("$GAMEDIR", self.gamedir)
return path_ref.replace("$GAMEDIR", self.target_path)
if path_type == "CACHE":
return path_ref.replace("$CACHE", settings.DATA_DIR)
if path_type == "HOME":
return path_ref.replace("$HOME", os.path.expanduser("~"))
def _get_move_paths(self, params):
""" Validate and converts raw data passed to 'move' """
for required_param in ('dst', 'src'):
if required_param not in params:
raise ScriptingError(
@ -114,7 +245,7 @@ class ScriptInterpreter(object):
% required_param, params
)
src_ref = params['src']
src = (self.files.get(src_ref)
src = (self.game_files.get(src_ref)
or self._substitute(src_ref, 'CACHE')
or self._substitute(src_ref, 'GAMEDIR'))
if not src:
@ -126,7 +257,14 @@ class ScriptInterpreter(object):
raise ScriptingError("Wrong value for 'dst' param", dst_ref)
return (src, dst)
def check_md5(self, data):
filename = self.game_files.get(data['file'])
_hash = calculate_md5(filename)
if _hash != data['value']:
raise ScriptingError("MD5 checksum mismatch", data)
def move(self, params):
""" Move a file or directory """
src, dst = self._get_move_paths(params)
if not os.path.exists(src):
self.errors.append("I can't move %s, it does not exist" % src)
@ -143,7 +281,7 @@ class ScriptInterpreter(object):
def extract(self, data):
""" Extracts a file, guessing the compression method """
filename = self.files.get(data.get('file'))
filename = self.game_files.get(data['file'])
if not filename:
logger.error("No file for '%s' in game files" % data)
return False
@ -152,10 +290,10 @@ class ScriptInterpreter(object):
return False
msg = "Extracting %s" % filename
logger.debug(msg)
self.set_status(msg)
self.parent.set_status(msg)
_, extension = os.path.splitext(filename)
if extension == ".zip":
extract.unzip(filename, self.game_dir)
extract.unzip(filename, self.target_path)
elif filename.endswith('.tgz') or filename.endswith('.tar.gz'):
extract.untar(filename, None)
elif filename.endswith('.tar.bz2'):
@ -164,85 +302,60 @@ class ScriptInterpreter(object):
logger.error("unrecognised file extension %s" % extension)
return False
def _delete(self, data):
print "Script has requested to delete %s" % data
# pylint: disable=R0904
class Installer(Gtk.Dialog):
class InstallerDialog(Gtk.Dialog):
""" Gtk Dialog used during the install process """
game_dir = None
download_progress = None
"""Installer class"""
def __init__(self, game, installer=False):
super(Installer, self).__init__()
self.set_size_request(500, 400)
self.lutris_config = None # Internal game config
if not game:
msg = "No game specified in this installer"
logger.error(msg)
ErrorDialog(msg)
return
self.game = game
self.game_slug = slugify(self.game)
self.description = False
self.download_index = 0
self.rules = {} # Content of yaml file
self.actions = []
self.errors = []
# Essential game information to create Lutris launcher
self.game_info = {}
# Dictionary of the files needed to install the game
self.gamefiles = {}
# # Fetch assets
# banner_url = settings.INSTALLER_URL + '%s.jpg' % self.game_slug
# banner_dest = join(settings.DATA_DIR, "banners/%s.jpg" % self.game_slug)
# http.download_asset(banner_url, banner_dest, True)
# icon_url = settings.INSTALLER_URL + 'icon/%s.jpg' % self.game_slug
# icon_dest = join(settings.DATA_DIR, "icons/%s.png" % self.game_slug)
# http.download_asset(icon_url, icon_dest, True)
if installer is False:
self.installer_path = join(settings.CACHE_DIR, self.game + ".yml")
else:
self.installer_path = installer
def __init__(self, game_ref):
Gtk.Dialog.__init__(self)
# Install location
# Dialog properties
self.set_size_request(600, 480)
self.set_default_size(600, 480)
self.set_resizable(False)
# Default signals
self.connect('destroy', lambda q: Gtk.main_quit())
# Interpreter
self.interpreter = ScriptInterpreter(game_ref, self)
self.interpreter.is_valid()
## GUI Setup
# Top label
self.status_label = Gtk.Label()
self.status_label.set_markup('<b>Select installation directory:</b>')
self.status_label.set_alignment(0, 0)
self.status_label.set_padding(20, 0)
self.vbox.pack_start(self.status_label, True, True, 2)
self.vbox.add(self.status_label)
success = self.pre_install()
self.location_entry = FileChooserEntry(default=self.game_dir)
self.location_entry.entry.connect('changed', self.set_game_dir)
self.vbox.add(self.location_entry)
if not success:
logger.error("Unable to install game")
else:
logger.info("Ready! Launching installer.")
self.download_progress = None
self.set_default_size(600, 480)
self.set_resizable(False)
self.connect('destroy', lambda q: Gtk.main_quit())
banner_path = join(settings.CACHE_DIR,
"banners/%s.jpg" % self.game_slug)
if os.path.exists(banner_path):
banner = Gtk.Image()
banner.set_from_file(banner_path)
self.vbox.pack_start(banner, False, False)
if self.description:
description = Gtk.Label()
description.set_markup(self.description)
description.set_padding(20, 20)
self.vbox.pack_start(description, True, True)
# Target chooser
default_path = self.interpreter.default_target
location_entry = FileChooserEntry(default=default_path)
location_entry.entry.connect('changed', self.on_target_changed)
self.vbox.add(location_entry)
self.widget_box = Gtk.HBox()
self.vbox.pack_start(self.widget_box, True, True, 0)
separator = Gtk.HSeparator()
self.vbox.pack_start(separator, True, True, 10)
# Separator
self.vbox.pack_start(Gtk.HSeparator(), True, True, 10)
# Install button
self.install_button = Gtk.Button('Install')
self.install_button.connect('clicked', self.download_game_files)
#self.install_button.set_sensitive(False)
self.install_button.connect('clicked', self.on_install_clicked)
self.action_buttons = Gtk.Alignment.new(0.95, 0.1, 0.15, 0)
self.action_buttons.add(self.install_button)
@ -250,205 +363,38 @@ class Installer(Gtk.Dialog):
self.vbox.pack_start(self.action_buttons, False, False, 0)
self.show_all()
def display_errors(self):
full_message = "\n\n".join(self.errors)
ErrorDialog(full_message)
def on_target_changed(self, text_entry):
""" Sets the installation target for the game """
self.interpreter.target_path = text_entry.get_text()
def download_installer(self):
""" Save the downloaded installer to disk. """
def on_install_clicked(self, button):
self.interpreter.run()
full_url = settings.INSTALLER_URL + self.game_slug + '.yml'
request = urllib2.Request(url=full_url)
try:
urllib2.urlopen(request)
except urllib2.URLError:
error_msg = "Server is unreachable at %s", full_url
logger.error(error_msg)
self.errors.append(error_msg)
success = False
else:
logger.debug("Downloading installer: %s" % full_url)
urllib.urlretrieve(full_url, self.installer_path)
success = True
return success
def ask_user_for_file(self):
dlg = FileDialog()
return dlg.get_uri()
def pre_install(self):
"""
Reads the installer and checks everything is OK before beginning
the install process.
"""
# Fetch assets
banner_url = settings.INSTALLER_URL + '%s.jpg' % self.game_slug
banner_dest = join(settings.DATA_DIR, "banners/%s.jpg" % self.game_slug)
http.download_asset(banner_url, banner_dest, True)
icon_url = settings.INSTALLER_URL + 'icon/%s.jpg' % self.game_slug
icon_dest = join(settings.DATA_DIR, "icons/%s.png" % self.game_slug)
http.download_asset(icon_url, icon_dest, True)
def set_status(self, text):
self.status_label.set_text(text)
# Download installer if not already there.
success = self.download_installer()
if not success:
self.display_errors()
return False
# Parse installer file
script_data = file(self.installer_path, 'r').read()
self.interpreter = ScriptInterpreter(script_data)
if not self.interpreter.is_valid():
raise ScriptingError("Installation script contains errors",
self.interpreter.errors)
success = self.parse_config()
games_dir = self.lutris_config.get_path()
if not games_dir:
logger.debug("No default path for %s games" % self.rules['runner'])
default_path = join(os.path.expanduser('~'), self.game_slug)
logger.debug("default path set to %s " % default_path)
self.game_dir = default_path
return True
self.game_dir = join(games_dir, self.game_slug)
logger.debug("Setting default path to : %s", self.game_dir)
if not os.path.exists(self.game_dir):
os.mkdir(self.game_dir)
return success
def set_game_dir(self, widget):
self.game_dir = widget.get_text()
def download_game_files(self, _widget=None, _data=None):
""" Runs the actions to complete the install. """
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
if not exists(dest_dir):
logger.debug('Creating destination directory %s' % dest_dir)
os.mkdir(dest_dir)
self.location_entry.destroy()
self.install_button.set_sensitive(False)
self.process_downloads()
def process_downloads(self):
"""Download each file needed for the game"""
files = self.rules.get('files', [])
if self.download_index < len(files):
logger.info(
"Downloading file %d of %d",
self.download_index + 1, len(self.rules["files"])
)
self.download_game_file(self.rules["files"][self.download_index])
else:
logger.info("All files downloaded")
self.install()
def start_download(self, file_uri, dest_file):
if self.download_progress:
# Remove existing progress bar
self.download_progress.destroy()
self.download_progress = DownloadProgressBox(
{'url': file_uri, 'dest': dest_file}, cancelable=True
)
self.download_progress.connect('complete', self.download_complete)
self.widget_box.pack_start(self.download_progress, True, True, 10)
self.download_progress.show()
self.download_progress.start()
def download_complete(self, widget=None, data=None):
"""Action called on a completed download"""
self.download_index += 1
self.process_downloads()
self.interpreter.iter_game_files()
def download_game_file(self, game_file):
"""Download a file referenced in the installer script
Game files can be either a string, containing the location of the
file to fetch or a dict with the possible options :
- url : location of file, if not present, filename will be used
this should be the case for local files
- filename : force destination filename when url is present or path
of local file
return the path of local file
"""
file_id = game_file.keys()[0]
if isinstance(game_file[file_id], dict):
filename = game_file[file_id].get('filename')
url = game_file[file_id].get('url', filename)
else:
url = game_file[file_id]
filename = None
logger.debug("Fetching [%s]: %s" % (file_id, url))
pga_url = pga.check_for_file(self.game_slug, file_id)
if pga_url:
url = pga_url
self.status_label.set_text('Fetching %s' % url)
dest_dir = join(settings.CACHE_DIR, "installer/%s" % self.game_slug)
if not filename:
filename = os.path.basename(url)
dest_file = os.path.join(dest_dir, filename)
if os.path.exists(dest_file):
logger.debug("Destination file exists")
os.remove(dest_file)
if url == "N/A":
if not filename:
#Ask the user where is located the file
dlg = FileDialog()
url = dlg.filename
if not filename:
self.errors.append("Installation cancelled")
return False
self.gamefiles[file_id] = dest_file
if url.startswith('/'):
shutil.copy(url, dest_dir)
dest_file = os.path.join(dest_dir, os.path.basename(url))
self.download_complete(data=os.path.join(
dest_dir, os.path.basename(filename)
))
elif url.startswith("http"):
if self.download_progress:
# Remove existing progress bar
self.download_progress.destroy()
self.download_progress = DownloadProgressBox(
{'url': url, 'dest': dest_file}, cancelable=True
)
self.download_progress.connect('complete', self.download_complete)
self.widget_box.pack_start(self.download_progress, True, True, 10)
self.download_progress.show()
self.download_progress.start()
def install(self):
def on_install_finished(self):
"""Actual game installation"""
logger.debug("Running installation")
if not os.path.exists(self.game_dir):
os.makedirs(self.game_dir)
os.chdir(self.game_dir)
for action in self.actions:
print action
if isinstance(action, dict):
action_name = action.keys()[0]
action_data = action[action_name]
else:
action_name = action
action_data = None
mappings = {
'insert-disc': self._insert_disc,
'extract': self._extract,
'move': self._move,
'run': self._run,
'runner': self._runner_task,
'check_md5': self._check_md5,
'delete': self._delete,
}
if not hasattr(ScriptInterpreter, action_name):
raise ScriptingError("The command %s does not exists"
% action_name)
if action_name not in mappings.keys():
logger.error("Action " + action_name + " not supported !")
continue
mappings[action_name](action_data)
if self.errors:
self.status_label.set_text("Installation error")
error_label = Gtk.Label()
error_label.set_line_wrap(True)
error_label.set_selectable(True)
error_label.set_markup("\n".join(self.errors))
error_label.show()
self.widget_box.pack_start(error_label, True, True, 20)
return False
self.status_label.set_text("Writing configuration")
self.write_config()
self.status_label.set_text("Installation finished !")
desktop_btn = Gtk.Button('Create a desktop shortcut')
@ -474,86 +420,22 @@ class Installer(Gtk.Dialog):
play_button.connect('clicked', self.launch_game)
self.action_buttons.add(play_button)
def write_config(self):
"""Write the game configuration as a Lutris launcher."""
config_filename = join(settings.CONFIG_DIR,
"games/%s.yml" % self.game_slug)
config_data = {
'game': {},
'realname': self.game_info['name'],
'runner': self.game_info['runner']
}
is_64bit = platform.machine() == "x86_64"
exe = 'exe64' if 'exe64' in self.game_info and is_64bit else 'exe'
for launcher in [exe, 'iso', 'rom', 'disk']:
if launcher in self.game_info:
if launcher == "exe64":
key = "exe"
else:
key = launcher
game_resource = self.game_info[launcher]
if type(game_resource) == list:
resource_paths = []
for res in game_resource:
if res in self.gamefiles:
resource_paths.append(self.gamefiles[res])
else:
resource_paths.append(res)
config_data['game'][key] = resource_paths
else:
if game_resource in self.gamefiles:
game_resource = self.gamefiles[game_resource]
else:
game_resource = join(self.game_dir, game_resource)
config_data['game'][key] = game_resource
yaml_config = yaml.safe_dump(config_data, default_flow_style=False)
with open(config_filename, "w") as config_file:
config_file.write(yaml_config)
def _get_path(self, data):
"""Return a filesystem path based on data"""
if data == 'parent':
path = os.path.dirname(self.game_dir)
else:
path = self.game_dir
return path
def _check_md5(self, data):
return True
print "MD5"
calculate_md5(self.gamefiles.get(data))
print self.gamefiles
print data
def _run(self, executable):
"""Run an executable script"""
exec_path = os.path.join(settings.CACHE_DIR, self.game_slug,
self.gamefiles[executable])
if not os.path.exists(exec_path):
print("unable to find %s" % exec_path)
exit()
else:
os.popen('chmod +x %s' % exec_path)
subprocess.call([exec_path])
def _runner_task(self, data):
""" This action triggers a task within a runner.
Mandatory parameters in data are 'task' and 'args'
"""
logger.info("Called runner task")
logger.debug(data)
logger.debug("runner is %s", self.rules['runner'])
runner_name = self.rules["runner"]
logger.debug("runner is %s", self.script['runner'])
runner_name = self.script["runner"]
task = import_task(runner_name, data['task'])
args = data['args']
for key in args:
if args[key] in ("$GAME_DIR", "$GAMEDIR"):
args[key] = self.game_dir
if key == 'filename':
if args[key] in self.gamefiles.keys():
args[key] = self.gamefiles[args[key]]
if args[key] in self.game_files.keys():
args[key] = self.game_files[args[key]]
logger.debug("args are %s", repr(args))
# FIXME pass args as kwargs and not args
task(**args)

View file

@ -18,26 +18,15 @@
"""Personnal Game Archive module. Handle local database of user's games."""
import os
import sqlite3
from lutris.util.strings import slugify
from lutris.util.log import logger
from lutris.util import sql
from lutris import settings
PGA_DB = settings.PGA_DB
class db_cursor():
def __enter__(self):
self.db_conn = sqlite3.connect(PGA_DB)
cursor = self.db_conn.cursor()
return cursor
def __exit__(self, type, value, traceback):
self.db_conn.commit()
self.db_conn.close()
def create_games(cursor):
create_game_table_query = """CREATE TABLE games (
id INTEGER PRIMARY KEY,
@ -62,33 +51,14 @@ def create_sources(cursor):
def create():
"""Create the local PGA database."""
logger.debug("Running CREATE statement...")
with db_cursor() as cursor:
with sql.db_cursor(PGA_DB) as cursor:
create_games(cursor)
create_sources(cursor)
def db_insert(table, fields):
field_names = ", ".join(fields.keys())
placeholders = ("?, " * len(fields))[:-2]
field_values = tuple(fields.values())
with db_cursor() as cursor:
cursor.execute(
"insert into {0}({1}) values ({2})".format(table,
field_names,
placeholders),
field_values
)
def db_delete(table, field, value):
with db_cursor() as cursor:
cursor.execute("delete from {0} where {1}=?".format(table, field),
(value,))
def get_games(name_filter=None):
"""Get the list of every game in database."""
with db_cursor() as cursor:
with sql.db_cursor(PGA_DB) as cursor:
if name_filter is not None:
query = "select * from games where name LIKE ?"
rows = cursor.execute(query, (name_filter, ))
@ -110,24 +80,24 @@ def add_game(name, runner, slug=None):
"""Adds a game to the PGA database."""
if not slug:
slug = slugify(name)
db_insert("games", {'name': name, 'slug': slug, 'runner': runner})
sql.db_insert("games", {'name': name, 'slug': slug, 'runner': runner})
def delete_game(name):
"""Deletes a game from the PGA"""
db_delete("games", 'name', name)
sql.db_delete(PGA_DB, "games", 'name', name)
def add_source(uri):
db_insert("sources", {"uri": uri})
sql.db_insert(PGA_DB, "sources", {"uri": uri})
def delete_source(uri):
db_delete("sources", 'uri', uri)
sql.db_delete(PGA_DB, "sources", 'uri', uri)
def read_sources():
with db_cursor() as cursor:
with sql.db_cursor(PGA_DB) as cursor:
rows = cursor.execute("select uri from sources")
results = rows.fetchall()
return [row[0] for row in results]
@ -137,10 +107,10 @@ def write_sources(sources):
db_sources = read_sources()
for uri in db_sources:
if uri not in sources:
db_delete("sources", 'uri', uri)
sql.db_delete(PGA_DB, "sources", 'uri', uri)
for uri in sources:
if uri not in db_sources:
db_insert("sources", {'uri': uri})
sql.db_insert(PGA_DB, "sources", {'uri': uri})
def check_for_file(game, file_id):
@ -158,11 +128,9 @@ def check_for_file(game, file_id):
continue
game_dir = os.path.join(source, game)
if not os.path.exists(game_dir):
print "dir", game_dir
continue
game_files = os.listdir(game_dir)
for game_file in game_files:
print "file", game_file
game_base, _ext = os.path.splitext(game_file)
if game_base == file_id:
return os.path.join(game_dir, game_file)

View file

@ -87,7 +87,7 @@ setup(
packages=['lutris', 'lutris.gui', 'lutris.util', 'lutris.runners'],
scripts=['bin/lutris'],
data_files=data_files,
install_requires=['PyYAML'],
install_requires=['PyYAML', 'pyxdg', 'PyGObject'],
url='http://lutris.net',
description='Install and play any video game on Linux',
long_description="""Lutris is a gaming platform for GNU/Linux. It's goal is

View file

@ -8,13 +8,20 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from lutris.util import http
from lutris.gui.dialogs import DownloadDialog
TEST_URL = "http://strycore.com/documents/serious.sam.tfe_1.05beta3-english-2.run"
#TEST_URL = "file:///usr/bin/gvim"
#TEST_URL = "http://strycore.com/documents/serious.sam.tfe_1.05beta3-english-2.run"
TEST_FILE_SIZE = 11034817
#TEST_URL = "ftp://ftp.3drealms.com/share/3dsw12.zip"
#TEST_URL = "ftp://ftp.idsoftware.com/idstuff/wolf/linux/wolf-linux-1.41b.x86.run"
#TEST_URL = "ftp://download.nvidia.com/XFree86/Linux-x86/319.23/NVIDIA-Linux-x86-319.23.run"
#TEST_URL = "http://strycore.com/documents/normality-en.7z"
TEST_URL = "smb://newport/games/linux/aquaria/aquaria-lnx-humble-bundle.mojo.run"
GObject.threads_init()
def timed(function):
def _wrapped(*args, **kwargs):
print ">",
start_time = time.time()
retval = function(*args, **kwargs)
total = time.time() - start_time
@ -35,6 +42,7 @@ def test_download_asset():
class DownloadDialogBenchmark(DownloadDialog):
def download_complete(self, _widget, _data):
print "Complete"
self.destroy()
Gtk.main_quit()
@ -45,8 +53,6 @@ def test_download_dialog():
Gtk.main()
test_urlretrieve()
time.sleep(5)
test_download_asset()
time.sleep(5)
test_download_dialog()
#test_urlretrieve()
#test_download_asset()