class Game(object): """This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(game=self.slug) if self.is_installed: runner_class = import_runner(self.runner_name) if runner_class: self.runner = runner_class(self.config) else: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def use_runtime(self, system_config): disable_runtime = system_config.get('disable_runtime') env_runtime = os.getenv('LUTRIS_RUNTIME') if env_runtime and env_runtime.lower() in ('0', 'off'): disable_runtime = True return not disable_runtime def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] restrict_to_display = system_config.get('display') if restrict_to_display: display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution: display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') prefix_command = system_config.get("prefix_command", '').strip() if prefix_command: launch_arguments.insert(0, prefix_command) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = [] if self.use_runtime(system_config): runtime64_path = os.path.join(settings.RUNTIME_DIR, "lib64") if os.path.exists(runtime64_path): ld_library_path.append(runtime64_path) runtime32_path = os.path.join(settings.RUNTIME_DIR, "lib32") if os.path.exists(runtime32_path): ld_library_path.append(runtime32_path) game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) ld_arg = 'LD_LIBRARY_PATH="{}:$LD_LIBRARY_PATH"'.format(ld_full) launch_arguments.insert(0, ld_arg) env = gameplay_info.get('env') or [] for var in env: launch_arguments.insert(0, var) killswitch = system_config.get('killswitch') self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.working_dir, killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(5000, self.poke_process) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def poke_process(self): """Watch game's process.""" if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """Quit the game and cleanup.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()
class Game(object): """" This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.game_thread = None self.heartbeat = None self.game_config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data['runner'] self.directory = game_data['directory'] self.name = game_data['name'] self.is_installed = bool(game_data['installed']) self.load_config() def get_runner(self): """ Return the runner's name """ return self.game_config['runner'] def load_config(self): """ Load the game's configuration. """ self.game_config = LutrisConfig(game=self.slug) if self.is_installed: if not self.game_config.is_valid(): logger.error("Invalid game config for %s" % self.slug) else: runner_class = import_runner(self.runner_name) self.runner = runner_class(self.game_config) def remove(self, from_library=False, from_disk=False): if from_disk: if os.path.exists(self.directory): shutil.rmtree(self.directory) if from_library: pga.delete_game(self.slug) else: pga.set_uninstalled(self.slug) self.game_config.remove() def prelaunch(self): """ Verify that the current game can be launched. """ if not self.runner.is_installed(): question = ("The required runner is not installed.\n" "Do you wish to install it now ?") install_runner_dialog = dialogs.QuestionDialog( {'question': question, 'title': "Required runner unavailable"}) if Gtk.ResponseType.YES == install_runner_dialog.result: self.runner.install() return False if hasattr(self.runner, 'prelaunch'): success = self.runner.prelaunch() return success return True def play(self): """ Launch the game. """ if not self.prelaunch(): return False gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if isinstance(gameplay_info, dict): if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] else: logger.error("Old method used for returning gameplay infos") launch_arguments = gameplay_info resolution = self.game_config.get_system('resolution') if resolution: desktop_control.change_resolution(resolution) if self.game_config.get_system('reset_pulse'): desktop_control.reset_pulse() if self.game_config.get_system('hide_panels'): self.desktop.hide_panels() oss_wrapper = self.game_config.get_system("oss_wrapper") if oss_wrapper: launch_arguments.insert(0, audio.get_oss_wrapper(oss_wrapper)) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = gameplay_info.get('ld_library_path') if ld_library_path: launch_arguments.insert( 0, 'LD_LIBRARY_PATH="{}"'.format(ld_library_path) ) killswitch = self.game_config.get_system('killswitch') self.heartbeat = GLib.timeout_add(5000, self.poke_process) self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.get_game_path(), killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = self.game_config.get_system('xboxdrv') if xboxdrv_config: self.xboxdrv(xboxdrv_config) def joy2key(self, config): """ Run a joy2key thread. """ win = "grep %s" % config['window'] if 'notwindow' in config: win = win + ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = "~/.joy2keyrc" command = "sleep 5 " command += "&& joy2key $(%s) -X -rcfile %s -buttons %s -axis %s" % ( wid, rcfile, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) thread = LutrisThread(command) thread.start() def poke_process(self): """ Watch game's process. """ if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """ Quit the game and cleanup. """ self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.game_config.get_system('resolution'): desktop_control.reset_desktop() if self.game_config.get_system('xboxdrv'): logger.debug("Shutting down xboxdrv") os.system("pkexec xboxdrvctl --shutdown") if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' self.exit_main_loop = False game_data = pga.get_game_by_field(id, 'id') self.slug = game_data.get('slug') or '' self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.platform = game_data.get('platform') or '' self.year = game_data.get('year') or '' self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def show_error_message(self, message): """Display an error message based on the runner's output.""" if "CUSTOM" == message['error']: message_text = message['text'].replace('&', '&') dialogs.ErrorDialog(message_text) elif "RUNNER_NOT_INSTALLED" == message['error']: dialogs.ErrorDialog('Error the runner is not installed') elif "NO_BIOS" == message['error']: dialogs.ErrorDialog("A bios file is required to run this game") elif "FILE_NOT_FOUND" == message['error']: filename = message['file'] if filename: message_text = "The file {} could not be found".format( filename.replace('&', '&') ) else: message_text = "No file provided" dialogs.ErrorDialog(message_text) elif "NOT_EXECUTABLE" == message['error']: message_text = message['file'].replace('&', '&') dialogs.ErrorDialog("The file %s is not executable" % message_text) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) def remove(self, from_library=False, from_disk=False): if from_disk and self.runner: logger.debug("Removing game %s from disk" % self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_game_by_field(self.slug, 'slug', all=True) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library" % self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) self.config.remove() shortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def save(self): self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() env = {} logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED return sdl_video_fullscreen = system_config.get('sdl_video_fullscreen') or '' env['SDL_VIDEO_FULLSCREEN_DISPLAY'] = sdl_video_fullscreen restrict_to_display = system_config.get('display') if restrict_to_display != 'off': display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution != 'off': display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() self.killswitch = system_config.get('killswitch') if self.killswitch and not os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(xephyr_command) xephyr_thread.start() time.sleep(3) env['DISPLAY'] = ':2' if system_config.get('use_us_layout'): setxkbmap_command = ['setxkbmap', '-model', 'pc101', 'us', '-print'] xkbcomp_command = ['xkbcomp', '-', os.environ.get('DISPLAY', ':0')] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() pulse_latency = system_config.get('pulse_latency') if pulse_latency: env['PULSE_LATENCY_MSEC'] = '60' prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) single_cpu = system_config.get('single_cpu') or False if single_cpu: logger.info('The game will run on a single CPU core') launch_arguments.insert(0, '0') launch_arguments.insert(0, '-c') launch_arguments.insert(0, 'taskset') terminal = system_config.get('terminal') if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED return # Env vars game_env = gameplay_info.get('env') or {} env.update(game_env) system_env = system_config.get('env') or {} env.update(system_env) ld_preload = gameplay_info.get('ld_preload') or '' env["LD_PRELOAD"] = ld_preload # Runtime management ld_library_path = "" if self.runner.use_runtime(): runtime_env = runtime.get_env() if 'STEAM_RUNTIME' in runtime_env and 'STEAM_RUNTIME' not in env: env['STEAM_RUNTIME'] = runtime_env['STEAM_RUNTIME'] if 'LD_LIBRARY_PATH' in runtime_env: ld_library_path = runtime_env['LD_LIBRARY_PATH'] game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join([game_ld_libary_path, ld_library_path]) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog("<b>Error lauching the game:</b>\n" + self.game_thread.error) self.on_game_quit() return False self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.path.exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False return True def stop(self): self.state = self.STATE_STOPPED if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.state = self.STATE_STOPPED os.chdir(os.path.expanduser('~')) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('use_us_layout'): subprocess.Popen(['setxkbmap'], env=os.environ).communicate() if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv') \ and self.xboxdrv_thread.is_running: self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop() self.process_return_codes() if self.exit_main_loop: exit() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog("<b>Error: Missing shared library.</b>" "\n\n%s" % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog("<b>Error: A different Wine version is " "already using the same Wine prefix.</b>")
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.exit_main_loop = False game_data = pga.get_game_by_field(id, 'id') self.slug = game_data.get('slug') or '' self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.platform = game_data.get('platform') or '' self.year = game_data.get('year') or '' self.lastplayed = game_data.get('lastplayed') or 0 self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.compositor_disabled = False self.stop_compositor = self.start_compositor = "" self.original_outputs = None self.log_buffer = Gtk.TextBuffer() self.log_buffer.create_tag("warning", foreground="red") def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def show_error_message(self, message): """Display an error message based on the runner's output.""" if "CUSTOM" == message['error']: message_text = message['text'].replace('&', '&') dialogs.ErrorDialog(message_text) elif "RUNNER_NOT_INSTALLED" == message['error']: dialogs.ErrorDialog('Error the runner is not installed') elif "NO_BIOS" == message['error']: dialogs.ErrorDialog("A bios file is required to run this game") elif "FILE_NOT_FOUND" == message['error']: filename = message['file'] if filename: message_text = "The file {} could not be found".format( filename.replace('&', '&') ) else: message_text = "No file provided" dialogs.ErrorDialog(message_text) elif "NOT_EXECUTABLE" == message['error']: message_text = message['file'].replace('&', '&') dialogs.ErrorDialog("The file %s is not executable" % message_text) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) def desktop_effects(self, enable): if enable: system.execute(self.start_compositor, shell=True) else: if os.environ.get('DESKTOP_SESSION') == "plasma": self.stop_compositor = "qdbus org.kde.KWin /Compositor org.kde.kwin.Compositing.suspend" self.start_compositor = "qdbus org.kde.KWin /Compositor org.kde.kwin.Compositing.resume" elif os.environ.get('DESKTOP_SESSION') == "mate" and system.execute("gsettings get org.mate.Marco.general compositing-manager", shell=True) == 'true': self.stop_compositor = "gsettings set org.mate.Marco.general compositing-manager false" self.start_compositor = "gsettings set org.mate.Marco.general compositing-manager true" elif os.environ.get('DESKTOP_SESSION') == "xfce" and system.execute("xfconf-query --channel=xfwm4 --property=/general/use_compositing", shell=True) == 'true': self.stop_compositor = "xfconf-query --channel=xfwm4 --property=/general/use_compositing --set=false" self.start_compositor = "xfconf-query --channel=xfwm4 --property=/general/use_compositing --set=true" if not (self.compositor_disabled or self.stop_compositor == ""): system.execute(self.stop_compositor, shell=True) self.compositor_disabled = True; def remove(self, from_library=False, from_disk=False): if from_disk and self.runner: logger.debug("Removing game %s from disk" % self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_games_where(slug=self.slug) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library" % self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) self.config.remove() xdg.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def set_platform_from_runner(self): if not self.runner: return self.platform = self.runner.get_platform() def save(self, metadata_only=False): """ Save the game's config and metadata, if `metadata_only` is set to True, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ if not metadata_only: self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, lastplayed=self.lastplayed, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = sorted( display.get_outputs(), key=lambda e: e[0] == system_config.get('display') ) gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED return env = {} sdl_gamecontrollerconfig = system_config.get('sdl_gamecontrollerconfig') if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if os.path.exists(path): with open(path, "r") as f: sdl_gamecontrollerconfig = f.read() env['SDL_GAMECONTROLLERCONFIG'] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get('sdl_video_fullscreen') or '' env['SDL_VIDEO_FULLSCREEN_DISPLAY'] = sdl_video_fullscreen restrict_to_display = system_config.get('display') if restrict_to_display != 'off': display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution != 'off': display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() self.killswitch = system_config.get('killswitch') if self.killswitch and not os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] optimus = system_config.get('optimus') if optimus == 'primusrun' and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') elif optimus == 'optirun' and system.find_executable('optirun'): launch_arguments.insert(0, 'virtualgl') launch_arguments.insert(0, '-b') launch_arguments.insert(0, 'optirun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(xephyr_command) xephyr_thread.start() time.sleep(3) env['DISPLAY'] = ':2' if system_config.get('use_us_layout'): setxkbmap_command = ['setxkbmap', '-model', 'pc101', 'us', '-print'] xkbcomp_command = ['xkbcomp', '-', os.environ.get('DISPLAY', ':0')] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() pulse_latency = system_config.get('pulse_latency') if pulse_latency: env['PULSE_LATENCY_MSEC'] = '60' prefix_command = system_config.get("prefix_command") or '' if prefix_command: launch_arguments = shlex.split(os.path.expandvars(prefix_command)) + launch_arguments single_cpu = system_config.get('single_cpu') or False if single_cpu: logger.info('The game will run on a single CPU core') launch_arguments.insert(0, '0') launch_arguments.insert(0, '-c') launch_arguments.insert(0, 'taskset') terminal = system_config.get('terminal') if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED return # Env vars game_env = gameplay_info.get('env') or self.runner.get_env() env.update(game_env) ld_preload = gameplay_info.get('ld_preload') if ld_preload: env["LD_PRELOAD"] = ld_preload game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join([game_ld_libary_path, ld_library_path]) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars include_processes = shlex.split(system_config.get('include_processes', '')) exclude_processes = shlex.split(system_config.get('exclude_processes', '')) monitoring_disabled = system_config.get('disable_monitoring') if monitoring_disabled: show_obnoxious_process_monitor_message() process_watch = not monitoring_disabled if self.runner.system_config.get('disable_compositor'): self.desktop_effects(False) self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), watch=process_watch, term=terminal, log_buffer=self.log_buffer, include_processes=include_processes, exclude_processes=exclude_processes) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING # xboxdrv setup xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if monitoring_disabled: logger.info("Process monitoring disabled") else: self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command, include_processes=['xboxdrv']) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog("<b>Error lauching the game:</b>\n" + self.game_thread.error) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = ( self.killswitch and not os.path.exists(self.killswitch) ) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False return True def stop(self): if self.runner.system_config.get('xboxdrv'): logger.debug("Stopping xboxdrv") self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=self.runner.killall_on_exit()) self.state = self.STATE_STOPPED def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None if self.state != self.STATE_STOPPED: logger.debug("Game thread still running, stopping it (state: %s)", self.state) self.stop() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser('~')) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.compositor_disabled: self.desktop_effects(True) if self.runner.system_config.get('use_us_layout'): subprocess.Popen(['setxkbmap'], env=os.environ).communicate() if self.runner.system_config.get('restore_gamma'): display.restore_gamma() self.process_return_codes() if self.exit_main_loop: exit() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog("<b>Error: Missing shared library.</b>" "\n\n%s" % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog("<b>Error: A different Wine version is " "already using the same Wine prefix.</b>") def notify_steam_game_changed(self, appmanifest): """Receive updates from Steam games and set the thread's ready state accordingly""" if 'Fully Installed' in appmanifest.states and not self.game_thread.ready_state: logger.info("Steam game %s is fully installed", appmanifest.steamid) self.game_thread.ready_state = True elif 'Update Required' in appmanifest.states and self.game_thread.ready_state: logger.info("Steam game %s updating, setting game thread as not ready", appmanifest.steamid) self.game_thread.ready_state = False
class Game(object): """This class takes cares about loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_slug=self.slug) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) self.runner = runner_class(self.config) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) def save(self): self.config.save() pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, directory=self.directory, installed=self.is_installed ) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def use_runtime(self, system_config): disable_runtime = system_config.get('disable_runtime') env_runtime = os.getenv('LUTRIS_RUNTIME') if env_runtime and env_runtime.lower() in ('0', 'off'): disable_runtime = True return not disable_runtime def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False restrict_to_display = system_config.get('display') if restrict_to_display != 'off': display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution != 'off': display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() self.killswitch = system_config.get('killswitch') if self.killswitch and not os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) terminal = system_config.get('terminal') if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) return False # Env vars env = {} game_env = gameplay_info.get('env') or {} env.update(game_env) ld_preload = gameplay_info.get('ld_preload') if ld_preload: env["LD_PRELOAD"] = ld_preload ld_library_path = [] if self.use_runtime(system_config): env['STEAM_RUNTIME'] = os.path.join(settings.RUNTIME_DIR, 'steam') ld_library_path += runtime.get_runtime_paths() game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) env["LD_LIBRARY_PATH"] = "{}:$LD_LIBRARY_PATH".format(ld_full) # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) self.state = self.STATE_RUNNING if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def beat(self): """Watch game's process.""" self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.path.exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: self.on_game_quit() return False return True def stop(self): self.game_thread.stop(killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) self.state = self.STATE_STOPPED if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' self.exit_main_loop = False game_data = pga.get_game_by_field(id, 'id') self.slug = game_data.get('slug') or '' self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) def remove(self, from_library=False, from_disk=False): if from_disk and self.runner: logger.debug("Removing game %s from disk" % self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_game_by_field(self.slug, 'slug', all=True) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library" % self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) self.config.remove() shortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def save(self): self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() env = {} logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) self.state = self.STATE_STOPPED return sdl_video_fullscreen = system_config.get('sdl_video_fullscreen') or '' env['SDL_VIDEO_FULLSCREEN_DISPLAY'] = sdl_video_fullscreen restrict_to_display = system_config.get('display') if restrict_to_display != 'off': display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution != 'off': display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() self.killswitch = system_config.get('killswitch') if self.killswitch and not os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(xephyr_command) xephyr_thread.start() time.sleep(3) env['DISPLAY'] = ':2' if system_config.get('use_us_layout'): setxkbmap_command = ['setxkbmap', '-model', 'pc101', 'us', '-print'] xkbcomp_command = ['xkbcomp', '-', os.environ.get('DISPLAY', ':0')] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() pulse_latency = system_config.get('pulse_latency') if pulse_latency: env['PULSE_LATENCY_MSEC'] = '60' prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) single_cpu = system_config.get('single_cpu') or False if single_cpu: logger.info('The game will run on a single CPU core') launch_arguments.insert(0, '0') launch_arguments.insert(0, '-c') launch_arguments.insert(0, 'taskset') terminal = system_config.get('terminal') if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED return # Env vars game_env = gameplay_info.get('env') or {} env.update(game_env) system_env = system_config.get('env') or {} env.update(system_env) ld_preload = gameplay_info.get('ld_preload') or '' env["LD_PRELOAD"] = ld_preload # Runtime management ld_library_path = "" if self.runner.use_runtime(): runtime_env = runtime.get_env() if 'STEAM_RUNTIME' in runtime_env and 'STEAM_RUNTIME' not in env: env['STEAM_RUNTIME'] = runtime_env['STEAM_RUNTIME'] if 'LD_LIBRARY_PATH' in runtime_env: ld_library_path = runtime_env['LD_LIBRARY_PATH'] game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join(game_ld_libary_path, ld_library_path) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog("<b>Error lauching the game:</b>\n" + self.game_thread.error) self.on_game_quit() return False self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.path.exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False return True def stop(self): self.state = self.STATE_STOPPED if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.state = self.STATE_STOPPED os.chdir(os.path.expanduser('~')) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('use_us_layout'): subprocess.Popen(['setxkbmap'], env=os.environ).communicate() if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv') \ and self.xboxdrv_thread.is_running: self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop() self.process_return_codes() if self.exit_main_loop: exit() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog("<b>Error: Missing shared library.</b>" "\n\n%s" % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog("<b>Error: A different Wine version is " "already using the same Wine prefix.</b>")
class Game(object): """This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(game=self.slug) if self.is_installed: runner_class = import_runner(self.runner_name) if runner_class: self.runner = runner_class(self.config) else: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] restrict_to_display = system_config.get('display') if restrict_to_display: display.turn_off_except(restrict_to_display) self.resolution_changed = True resolution = system_config.get('resolution') if resolution: display.change_resolution(resolution) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() prefix_command = system_config.get("prefix_command", '').strip() if prefix_command and system.find_executable(prefix_command): launch_arguments.insert(0, prefix_command) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = [] runtime64_path = os.path.join(settings.RUNTIME_DIR, "lib64") if os.path.exists(runtime64_path): ld_library_path.append(runtime64_path) runtime32_path = os.path.join(settings.RUNTIME_DIR, "lib32") if os.path.exists(runtime32_path): ld_library_path.append(runtime32_path) game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) ld_arg = 'LD_LIBRARY_PATH="{}:$LD_LIBRARY_PATH"'.format(ld_full) launch_arguments.insert(0, ld_arg) env = gameplay_info.get('env') or [] for var in env: launch_arguments.insert(0, var) killswitch = system_config.get('killswitch') self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.working_dir, killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(5000, self.poke_process) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def poke_process(self): """Watch game's process.""" if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """Quit the game and cleanup.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.exit_main_loop = False game_data = pga.get_game_by_field(id, 'id') self.slug = game_data.get('slug') or '' self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.platform = game_data.get('platform') or '' self.year = game_data.get('year') or '' self.lastplayed = game_data.get('lastplayed') or 0 self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.original_outputs = None self.log_buffer = Gtk.TextBuffer() self.log_buffer.create_tag("warning", foreground="red") def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def show_error_message(self, message): """Display an error message based on the runner's output.""" if "CUSTOM" == message['error']: message_text = message['text'].replace('&', '&') dialogs.ErrorDialog(message_text) elif "RUNNER_NOT_INSTALLED" == message['error']: dialogs.ErrorDialog('Error the runner is not installed') elif "NO_BIOS" == message['error']: dialogs.ErrorDialog("A bios file is required to run this game") elif "FILE_NOT_FOUND" == message['error']: filename = message['file'] if filename: message_text = "The file {} could not be found".format( filename.replace('&', '&') ) else: message_text = "No file provided" dialogs.ErrorDialog(message_text) elif "NOT_EXECUTABLE" == message['error']: message_text = message['file'].replace('&', '&') dialogs.ErrorDialog("The file %s is not executable" % message_text) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) def remove(self, from_library=False, from_disk=False): if from_disk and self.runner: logger.debug("Removing game %s from disk" % self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_games_where(slug=self.slug) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library" % self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) self.config.remove() xdg.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def set_platform_from_runner(self): if not self.runner: return self.platform = self.runner.get_platform() def save(self, metadata_only=False): """ Save the game's config and metadata, if `metadata_only` is set to True, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ if not metadata_only: self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, lastplayed=self.lastplayed, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = sorted( display.get_outputs(), key=lambda e: e[0] == system_config.get('display') ) gameplay_info = self.runner.play() env = {} logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED return sdl_gamecontrollerconfig = system_config.get('sdl_gamecontrollerconfig') if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if os.path.exists(path): with open(path, "r") as f: sdl_gamecontrollerconfig = f.read() env['SDL_GAMECONTROLLERCONFIG'] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get('sdl_video_fullscreen') or '' env['SDL_VIDEO_FULLSCREEN_DISPLAY'] = sdl_video_fullscreen restrict_to_display = system_config.get('display') if restrict_to_display != 'off': display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get('resolution') if resolution != 'off': display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() self.killswitch = system_config.get('killswitch') if self.killswitch and not os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(xephyr_command) xephyr_thread.start() time.sleep(3) env['DISPLAY'] = ':2' if system_config.get('use_us_layout'): setxkbmap_command = ['setxkbmap', '-model', 'pc101', 'us', '-print'] xkbcomp_command = ['xkbcomp', '-', os.environ.get('DISPLAY', ':0')] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() pulse_latency = system_config.get('pulse_latency') if pulse_latency: env['PULSE_LATENCY_MSEC'] = '60' prefix_command = system_config.get("prefix_command") or '' if prefix_command: launch_arguments = shlex.split(os.path.expandvars(prefix_command)) + launch_arguments single_cpu = system_config.get('single_cpu') or False if single_cpu: logger.info('The game will run on a single CPU core') launch_arguments.insert(0, '0') launch_arguments.insert(0, '-c') launch_arguments.insert(0, 'taskset') terminal = system_config.get('terminal') if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED return # Env vars game_env = gameplay_info.get('env') or {} env.update(game_env) ld_preload = gameplay_info.get('ld_preload') if (ld_preload): env["LD_PRELOAD"] = ld_preload game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join([game_ld_libary_path, ld_library_path]) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars include_processes = shlex.split(system_config.get('include_processes', '')) exclude_processes = shlex.split(system_config.get('exclude_processes', '')) monitoring_disabled = system_config.get('disable_monitoring') process_watch = not monitoring_disabled self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), watch=process_watch, term=terminal, log_buffer=self.log_buffer, include_processes=include_processes, exclude_processes=exclude_processes) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING # xboxdrv setup xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if monitoring_disabled: logger.info("Process monitoring disabled") else: self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command, include_processes=['xboxdrv']) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog("<b>Error lauching the game:</b>\n" + self.game_thread.error) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = ( self.killswitch and not os.path.exists(self.killswitch) ) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False return True def stop(self): if self.runner.system_config.get('xboxdrv'): logger.debug("Stopping xboxdrv") self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=self.runner.killall_on_exit()) self.state = self.STATE_STOPPED def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None if self.state != self.STATE_STOPPED: logger.debug("Game thread still running, stopping it (state: %s)", self.state) self.stop() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser('~')) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('use_us_layout'): subprocess.Popen(['setxkbmap'], env=os.environ).communicate() if self.runner.system_config.get('restore_gamma'): display.restore_gamma() self.process_return_codes() if self.exit_main_loop: exit() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog("<b>Error: Missing shared library.</b>" "\n\n%s" % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog("<b>Error: A different Wine version is " "already using the same Wine prefix.</b>") def notify_steam_game_changed(self, appmanifest): logger.debug("Steam game %s state has changed, new states: %s", appmanifest.steamid, ', '.join(appmanifest.states)) if 'Fully Installed' in appmanifest.states: self.game_thread.ready_state = True elif 'Update Required' in appmanifest.states: self.game_thread.ready_state = False