def write_json(self, params): """Write data into a json file.""" self._check_required_params(["file", "data"], params, "write_json") # Get file filename = self._get_file(params["file"]) # Create dir if necessary basedir = os.path.dirname(filename) if not os.path.exists(basedir): os.makedirs(basedir) merge = params.get("merge", True) if not os.path.exists(filename): # create an empty file with open(filename, "a+"): pass with open(filename, "r+" if merge else "w") as json_file: json_data = {} if merge: try: json_data = json.load(json_file) except ValueError: logger.error("Failed to parse JSON from file %s", filename) json_data = selective_merge(json_data, params.get("data", {})) json_file.seek(0) json_file.write(json.dumps(json_data, indent=2))
def parse(self): """Converts the glxinfo output to class attributes""" if not self._output: logger.error("No available glxinfo output") return # Fix glxinfo output (Great, you saved one line by # combining display and screen) output = self._output.replace(" screen", "\nscreen") for line in output.split("\n"): if not line.strip(): continue key, value = line.split(":", 1) key = key.replace(" string", "").replace(" ", "_") value = value.strip() if not value and key.startswith(("Extended_renderer_info", "Memory_info")): self._section = key[key.index("(") + 1:-1] setattr(self, self._section, Container()) continue if self._section: if not key.startswith("____"): self._section = None else: setattr(getattr(self, self._section), key.strip("_").lower(), value) continue self._attrs.add(key.lower()) setattr(self, key.lower(), value)
def prelaunch(self): super(winesteam, self).prelaunch() def check_shutdown(is_running, times=10): for x in range(1, times): time.sleep(1) if not is_running(): return True # Stop Wine Steam to prevent Wine prefix/version problems if is_running(): logger.info("Waiting for Steam to shutdown...") self.shutdown() if not check_shutdown(is_running): logger.info("Wine Steam does not shut down, killing it...") kill() if not check_shutdown(is_running, 5): logger.error("Failed to shut down Wine Steam :(") return False # Stop Linux Steam from lutris.runners import steam if steam.is_running(): logger.info("Waiting for Steam shutdown...") steam.shutdown() if not check_shutdown(steam.is_running): logger.info("Steam does not shut down, killing it...") steam.kill() if not check_shutdown(steam.is_running, 5): logger.error("Failed to shut down Steam :(") return False return True
def get_games(game_slugs=None, page=1): url = settings.SITE_URL + "/api/games" if int(page) > 1: url += "?page={}".format(page) response = http.Request(url, headers={'Content-Type': 'application/json'}) if game_slugs: payload = json.dumps({'games': game_slugs, 'page': page}).encode('utf-8') else: payload = None response.get(data=payload) response_data = response.json if not response_data: logger.warning('Unable to get games from API') return None results = response_data.get('results', []) while response_data.get('next'): page_match = re.search(r'page=(\d+)', response_data['next']) if page_match: page = page_match.group(1) else: logger.error("No page found in %s", response_data['next']) break page_result = get_games(game_slugs=game_slugs, page=page) if not page_result: logger.warning("Unable to get response for page %s", page) break else: results += page_result return results
def __init__(self, game_slug, file_id, file_meta): self.game_slug = game_slug self.id = file_id # pylint: disable=invalid-name self.dest_file = None if isinstance(file_meta, dict): for field in ("url", "filename"): if field not in file_meta: raise ScriptingError( "missing field `%s` for file `%s`" % (field, file_id) ) self.url = file_meta["url"] self.filename = file_meta["filename"] self.referer = file_meta.get("referer") self.checksum = file_meta.get("checksum") else: self.url = file_meta self.filename = os.path.basename(file_meta) self.referer = None self.checksum = None if self.url.startswith(("$STEAM", "$WINESTEAM")): self.filename = self.url if self.url.startswith("/"): self.url = "file://" + self.url if not self.filename: logger.error("Couldn't find a filename for file %s in %s", file_id, file_meta) raise ScriptingError( "No filename provided for %s, please provide 'url' " "and 'filename' parameters in the script" % file_id ) if self.uses_pga_cache(create=True): logger.debug("Using cache path %s", self.cache_path)
def get_outputs(): """Return list of tuples containing output name and geometry.""" outputs = [] vid_modes = get_vidmodes() if not vid_modes: logger.error("xrandr didn't return anything") return [] for line in vid_modes: parts = line.split() if len(parts) < 2: continue if parts[1] == 'connected': if len(parts) == 2: continue if parts[2] != 'primary': geom = parts[2] rotate = parts[3] else: geom = parts[3] rotate = parts[4] if geom.startswith('('): # Screen turned off, no geometry continue if rotate.startswith('('): # Screen not rotated, no need to include outputs.append((parts[0], geom, "normal")) else: if rotate in ("left", "right"): geom_parts = geom.split('+') x_y = geom_parts[0].split('x') geom = "{}x{}+{}+{}".format(x_y[1], x_y[0], geom_parts[1], geom_parts[2]) outputs.append((parts[0], geom, rotate)) return outputs
def create_prefix(prefix, wine_path=None, arch='win32'): """Create a new Wine prefix.""" logger.debug("Creating a %s prefix in %s", arch, prefix) # Avoid issue of 64bit Wine refusing to create win32 prefix # over an existing empty folder. if os.path.isdir(prefix) and not os.listdir(prefix): os.rmdir(prefix) if not wine_path: wine_path = wine().get_executable() wineboot_path = os.path.join(os.path.dirname(wine_path), 'wineboot') env = { 'WINEARCH': arch, 'WINEPREFIX': prefix } system.execute([wineboot_path], env=env) for i in range(20): time.sleep(.25) if os.path.exists(os.path.join(prefix, 'user.reg')): break if not os.path.exists(os.path.join(prefix, 'user.reg')): logger.error('No user.reg found after prefix creation. ' 'Prefix might not be valid') logger.info('%s Prefix created in %s', arch, prefix) prefix_manager = WinePrefixManager(prefix) prefix_manager.setup_defaults()
def kill_pid(pid): try: int(pid) except ValueError: logger.error("Invalid pid %s") return execute(["kill", "-9", pid])
def execute(command, env=None, cwd=None, log_errors=False): """Execute a system command and return its results.""" existing_env = os.environ.copy() if env: existing_env.update(env) logger.debug(' '.join('{}={}'.format(k, v) for k, v in env.iteritems())) logger.debug("Executing %s", ' '.join(command)) # Piping stderr can cause slowness in the programs, use carefully # (especially when using regedit with wine) if log_errors: stderr_config = subprocess.PIPE else: stderr_config = None try: stdout, stderr = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=stderr_config, env=existing_env, cwd=cwd).communicate() except OSError as ex: logger.error('Could not run command %s: %s', command, ex) return if stderr and log_errors: logger.error(stderr) return stdout.strip()
def search_games(query): if not query: return [] query = query.lower().strip()[:32] if query == "open source games": url = "/api/bundles/open-source" elif query == "free to play games": url = "/api/bundles/free-to-play" else: url = "/api/games?%s" % urllib.parse.urlencode({"search": query}) response = http.Request(settings.SITE_URL + url, headers={"Content-Type": "application/json"}) try: response.get() except http.HTTPError as ex: logger.error("Unable to get games from API: %s", ex) return None response_data = response.json if "bundles" in url: api_games = response_data.get("games", []) else: api_games = response_data.get("results", []) for index, game in enumerate(api_games, 1): game["id"] = index * -1 game["installed"] = 1 game["runner"] = None game["platform"] = None game["lastplayed"] = None game["installed_at"] = None game["playtime"] = None return api_games
def get_api_games(game_slugs=None, page="1", query_type="games", inject_aliases=False): """Return all games from the Lutris API matching the given game slugs""" response_data = get_game_api_page(game_slugs, page=page, query_type=query_type) if not response_data: return [] results = response_data.get("results", []) while response_data.get("next"): page_match = re.search(r"page=(\d+)", response_data["next"]) if page_match: next_page = page_match.group(1) else: logger.error("No page found in %s", response_data["next"]) break response_data = get_game_api_page(game_slugs, page=next_page, query_type=query_type) if not response_data.get("results"): logger.warning("Unable to get response for page %s", next_page) break else: results += response_data.get("results") if game_slugs and inject_aliases: matched_games = [] for game in results: for alias_slug in [alias["slug"] for alias in game.get("aliases", [])]: if alias_slug in game_slugs: matched_games.append((alias_slug, game)) for alias_slug, game in matched_games: game["slug"] = alias_slug results.append(game) return results
def get_overrides_env(overrides): """ Output a string of dll overrides usable with WINEDLLOVERRIDES See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides """ if not overrides: return "" override_buckets = OrderedDict( [("n,b", []), ("b,n", []), ("b", []), ("n", []), ("d", []), ("", [])] ) for dll, value in overrides.items(): if not value: value = "" value = value.replace(" ", "") value = value.replace("builtin", "b") value = value.replace("native", "n") value = value.replace("disabled", "") try: override_buckets[value].append(dll) except KeyError: logger.error("Invalid override value %s", value) continue override_strings = [] for value, dlls in override_buckets.items(): if not dlls: continue override_strings.append("{}={}".format(",".join(sorted(dlls)), value)) return ";".join(override_strings)
def on_done(self, _result, error): if error: logger.error("Download failed: %s", error) self.state = self.ERROR self.error = error if self.file_pointer: self.file_pointer.close() self.file_pointer = None return if self.state == self.CANCELLED: return logger.debug("Finished downloading %s", self.url) if not self.downloaded_size: logger.warning("Downloaded file is empty") if not self.full_size: self.progress_fraction = 1.0 self.progress_percentage = 100 self.state = self.COMPLETED self.file_pointer.close() self.file_pointer = None if self.callback: self.callback()
def init_versions(manager): try: manager.DXVK_VERSIONS \ = get_dxvk_versions(manager.base_name, manager.DXVK_TAGS_URL) except Exception as ex: # pylint: disable= broad-except logger.error(ex) manager.DXVK_LATEST, manager.DXVK_PAST_RELEASES = manager.DXVK_VERSIONS[0], manager.DXVK_VERSIONS[1:9]
def enable(self): """Enable DXVK for the current prefix""" if not system.path_exists(self.dxvk_path): logger.error(self.base_name.upper()+" %s is not available locally", self.version) return for system_dir, dxvk_arch, dll in self._iter_dxvk_dlls(): self.enable_dxvk_dll(system_dir, dxvk_arch, dll)
def execute(command, env=None, cwd=None, log_errors=False, quiet=False): """Execute a system command and return its results.""" existing_env = os.environ.copy() if env: existing_env.update(env) logger.debug(' '.join('{}={}'.format(k, v) for k, v in env.items())) if not quiet: logger.debug("Executing %s", ' '.join(command)) # Piping stderr can cause slowness in the programs, use carefully # (especially when using regedit with wine) if log_errors: stderr_handler = subprocess.PIPE stderr_needs_closing = False else: stderr_handler = open(os.devnull, 'w') stderr_needs_closing = True try: stdout, stderr = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=stderr_handler, env=existing_env, cwd=cwd).communicate() except OSError as ex: logger.error('Could not run command %s: %s', command, ex) return finally: if stderr_needs_closing: stderr_handler.close() if stderr and log_errors: logger.error(stderr) return stdout.decode(errors='replace').strip()
def get_gog_download_links(self): """Return a list of downloadable links for a GOG game""" gog_service = GogService() if not gog_service.is_available(): logger.info("You are not connected to GOG") connect_gog() if not gog_service.is_available(): raise UnavailableGame gog_installers = self.get_gog_installers(gog_service) if len(gog_installers) > 1: raise ScriptingError("Don't know how to deal with multiple installers yet.") try: installer = gog_installers[0] except IndexError: raise UnavailableGame download_links = [] for game_file in installer.get('files', []): downlink = game_file.get("downlink") if not downlink: logger.error("No download information for %s", installer) continue download_info = gog_service.get_download_info(downlink) for field in ('checksum', 'downlink'): url = download_info[field] logger.info("Adding %s to download links", url) download_links.append(download_info[field]) return download_links
def kill_pid(pid): try: int(pid) except ValueError: logger.error("Invalid pid %s") return execute(['kill', '-9', pid])
def setup_x360ce(self, x360ce_path): if not os.path.isdir(x360ce_path): logger.error("%s is not a valid path for x360ce", x360ce_path) return mode = 'dumbxinputemu' if self.runner_config.get('dumbxinputemu') else 'x360ce' dll_files = ['xinput1_3.dll'] if self.runner_config.get('x360ce-xinput9'): dll_files.append('xinput9_1_0.dll') for dll_file in dll_files: xinput_dest_path = os.path.join(x360ce_path, dll_file) xinput_arch = self.runner_config.get('xinput-arch') or self.wine_arch dll_path = os.path.join(datapath.get(), 'controllers/{}-{}'.format(mode, xinput_arch)) if not os.path.exists(xinput_dest_path): source_file = dll_file if mode == 'dumbxinputemu' else 'xinput1_3.dll' shutil.copyfile(os.path.join(dll_path, source_file), xinput_dest_path) if mode == 'x360ce': if self.runner_config.get('x360ce-dinput'): dinput8_path = os.path.join(dll_path, 'dinput8.dll') dinput8_dest_path = os.path.join(x360ce_path, 'dinput8.dll') shutil.copyfile(dinput8_path, dinput8_dest_path) x360ce_config = X360ce() x360ce_config.populate_controllers() x360ce_config.write(os.path.join(x360ce_path, 'x360ce.ini'))
def get_pixbuf_for_game(game_slug, icon_type, is_installed=True): if icon_type.startswith("banner"): default_icon_path = os.path.join(datapath.get(), "media/default_banner.png") icon_path = resources.get_banner_path(game_slug) elif icon_type.startswith("icon"): default_icon_path = os.path.join(datapath.get(), "media/default_icon.png") icon_path = resources.get_icon_path(game_slug) else: logger.error("Invalid icon type '%s'", icon_type) return None size = IMAGE_SIZES[icon_type] pixbuf = get_pixbuf(icon_path, size, fallback=default_icon_path) if not is_installed: unavailable_game_overlay = os.path.join(datapath.get(), "media/unavailable.png") transparent_pixbuf = get_overlay(unavailable_game_overlay, size).copy() pixbuf.composite( transparent_pixbuf, 0, 0, size[0], size[1], 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 100, ) return transparent_pixbuf return pixbuf
def sync_with_lutris(): desktop_games = { game['slug']: game for game in pga.get_games_where(runner='linux', installer_slug=INSTALLER_SLUG, installed=1) } seen = set() for name, appid, exe, args in get_games(): slug = slugify(name) or slugify(appid) if not all([name, slug, appid]): logger.error("Failed to load desktop game \"{}\" (app: {}, slug: {})".format(name, appid, slug)) continue else: logger.info("Found desktop game \"{}\" (app: {}, slug: {})".format(name, appid, slug)) seen.add(slug) if slug not in desktop_games.keys(): game_info = { 'name': name, 'slug': slug, 'config_path': slug + '-' + INSTALLER_SLUG, 'installer_slug': INSTALLER_SLUG, 'exe': exe, 'args': args } mark_as_installed(appid, 'linux', game_info) for slug in set(desktop_games.keys()).difference(seen): mark_as_uninstalled(desktop_games[slug])
def get_runner_version(self, version=None): logger.info( "Getting runner information for %s%s", self.name, "(version: %s)" % version if version else "", ) runner_info = self.get_runner_info() if not runner_info: logger.error("Failed to get runner information") return versions = runner_info.get("versions") or [] arch = self.arch if version: if version.endswith("-i386") or version.endswith("-x86_64"): version, arch = version.rsplit("-", 1) versions = [v for v in versions if v["version"] == version] versions_for_arch = [v for v in versions if v["architecture"] == arch] if len(versions_for_arch) == 1: return versions_for_arch[0] if len(versions_for_arch) > 1: default_version = [v for v in versions_for_arch if v["default"] is True] if default_version: return default_version[0] elif len(versions) == 1 and system.LINUX_SYSTEM.is_64_bit: return versions[0] elif len(versions) > 1 and system.LINUX_SYSTEM.is_64_bit: default_version = [v for v in versions if v["default"] is True] if default_version: return default_version[0] # If we didn't find a proper version yet, return the first available. if len(versions_for_arch) >= 1: return versions_for_arch[0]
def fix_path_case(path): """Do a case insensitive check, return the real path with correct case.""" if os.path.exists(path): return path parts = path.strip('/').split('/') current_path = "/" for part in parts: if not os.path.exists(current_path): return tested_path = os.path.join(current_path, part) if os.path.exists(tested_path): current_path = tested_path continue try: path_contents = os.listdir(current_path) except OSError: logger.error("Can't read contents of %s", current_path) path_contents = [] for filename in path_contents: if filename.lower() == part.lower(): current_path = os.path.join(current_path, filename) continue # Only return the path if we got the same number of elements if len(parts) == len(current_path.strip('/').split('/')): return current_path
def check_libs(all_components=False): """Checks that required libraries are installed on the system""" missing_libs = LINUX_SYSTEM.get_missing_libs() if all_components: components = LINUX_SYSTEM.requirements else: components = LINUX_SYSTEM.critical_requirements missing_vulkan_libs = [] for req in components: for index, arch in enumerate(LINUX_SYSTEM.runtime_architectures): for lib in missing_libs[req][index]: if req == "VULKAN": missing_vulkan_libs.append(arch) logger.error("%s %s missing (needed by %s)", arch, lib, req.lower()) if missing_vulkan_libs: setting = "dismiss-missing-vulkan-library-warning" if settings.read_setting(setting) != "True": DontShowAgainDialog( setting, "Missing vulkan libraries", secondary_message="The Vulkan library for %s has not been found. " "This will prevent games using Vulkan (such as DXVK games) from running. " "To install it, please follow " "<a href='https://github.com/lutris/lutris/wiki/Installing-drivers'>" "the instructions on our Wiki</a>" % " and ".join(missing_vulkan_libs) )
def get_glxinfo_output(): """Return the glxinfo -B output""" try: return subprocess.check_output(["glxinfo", "-B"]).decode() except subprocess.CalledProcessError as ex: logger.error("glxinfo call failed: %s", ex) return ""
def get_overrides_env(overrides): """ Output a string of dll overrides usable with WINEDLLOVERRIDES See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides """ if not overrides: return '' override_buckets = OrderedDict([ ('n,b', []), ('b,n', []), ('b', []), ('n', []), ('', []) ]) for dll, value in overrides.items(): if not value: value = '' value = value.replace(' ', '') value = value.replace('builtin', 'b') value = value.replace('native', 'n') value = value.replace('disabled', '') try: override_buckets[value].append(dll) except KeyError: logger.error('Invalid override value %s', value) continue override_strings = [] for value, dlls in override_buckets.items(): if not dlls: continue override_strings.append("{}={}".format(','.join(sorted(dlls)), value)) return ';'.join(override_strings)
def vdf_parse(steam_config_file, config): """Parse a Steam config file and return the contents as a dict.""" line = " " while line: try: line = steam_config_file.readline() except UnicodeDecodeError: logger.error("Error while reading Steam VDF file %s. Returning %s", steam_config_file, config) return config if not line or line.strip() == "}": return config while not line.strip().endswith("\""): nextline = steam_config_file.readline() if not nextline: break line = line[:-1] + nextline line_elements = line.strip().split("\"") if len(line_elements) == 3: key = line_elements[1] steam_config_file.readline() # skip '{' config[key] = vdf_parse(steam_config_file, {}) else: try: config[line_elements[1]] = line_elements[3] except IndexError: logger.error("Malformed config file: %s", line) return config
def get_game_api_page(game_ids, page="1", query_type="games"): """Read a single page of games from the API and return the response Args: game_ids (list): list of game IDs, the ID type is determined by `query_type` page (str): Page of results to get query_type (str): Type of the IDs in game_ids, by default 'games' queries games by their Lutris slug. 'gogid' can also be used. """ url = settings.SITE_URL + "/api/games" if int(page) > 1: url += "?page={}".format(page) response = http.Request(url, headers={"Content-Type": "application/json"}) if game_ids: payload = json.dumps({query_type: game_ids, "page": page}).encode("utf-8") else: raise ValueError("No game id provided will fetch all games from the API") try: response.get(data=payload) except http.HTTPError as ex: logger.error("Unable to get games from API: %s", ex) return None response_data = response.json num_games = len(response_data.get("results")) if num_games: logger.debug("Loaded %s games from page %s", num_games, page) else: logger.debug("No game found for %s", ', '.join(game_ids)) if not response_data: logger.warning("Unable to get games from API, status code: %s", response.status_code) return None return response_data
def play(self): rompath = self.runner_config.get("rompath") or "" if not system.path_exists(rompath): logger.warning("BIOS path provided in %s doesn't exist", rompath) rompath = os.path.join(settings.RUNNER_DIR, "mess/bios") if not system.path_exists(rompath): logger.error("Couldn't find %s", rompath) return {"error": "NO_BIOS"} machine = self.game_config.get("machine") if not machine: return {"error": "INCOMPLETE_CONFIG"} rom = self.game_config.get("main_file") or "" if rom and not system.path_exists(rom): return {"error": "FILE_NOT_FOUND", "file": rom} device = self.game_config.get("device") command = [self.get_executable()] if self.runner_config.get("uimodekey"): command += ["-uimodekey", self.runner["uimodekey"]] command += ["-rompath", rompath, machine] if device: command.append("-" + device) if rom: command.append(rom) return {"command": command}
def play(self): game_exe = self.game_exe arguments = self.game_config.get('args') or '' if not os.path.exists(game_exe): return {'error': 'FILE_NOT_FOUND', 'file': game_exe} launch_info = {} launch_info['env'] = self.get_env(full=False) if self.runner_config.get('xinput'): xinput_path = self.get_xinput_path() if xinput_path: logger.debug('Preloading %s', xinput_path) launch_info['ld_preload'] = self.get_xinput_path() else: logger.error('Missing koku-xinput-wine.so, Xinput won\'t be enabled') command = [self.get_executable()] if game_exe.endswith(".msi"): command.append('msiexec') command.append('/i') if game_exe.endswith('.lnk'): command.append('start') command.append('/unix') command.append(game_exe) if arguments: for arg in shlex.split(arguments): command.append(arg) launch_info['command'] = command return launch_info
def load(self): """Load the user game library from the GOG API""" if self.is_loading: logger.warning("GOG games are already loading") return if not self.is_connected(): logger.error("User not connected to GOG") return self.is_loading = True self.emit("service-games-load") games = [ GOGGame.new_from_gog_game(game) for game in self.get_library() ] for game in games: game.save() self.match_games() self.is_loading = False self.emit("service-games-loaded") return games
def enable_dxvk_dll(self, system_dir, dxvk_arch, dll): """Copies DXVK dlls to the appropriate destination""" # Copying DXVK's version dxvk_dll_path = os.path.join(self.dxvk_path, dxvk_arch, "%s.dll" % dll) if system.path_exists(dxvk_dll_path): wine_dll_path = os.path.join(system_dir, "%s.dll" % dll) logger.debug("Replacing %s/%s with DXVK version", system_dir, dll) if system.path_exists(wine_dll_path): if not self.is_dxvk_dll(wine_dll_path) and not os.path.islink(wine_dll_path): # Backing up original version (may not be needed) shutil.move(wine_dll_path, wine_dll_path + ".orig") else: os.remove(wine_dll_path) try: os.symlink(dxvk_dll_path, wine_dll_path) except OSError: logger.error("Failed linking %s to %s", dxvk_dll_path, wine_dll_path) else: self.disable_dxvk_dll(system_dir, dxvk_arch, dll)
def load(self): """Load the list of games""" if self.is_loading: logger.warning("EGS games are already loading") return self.is_loading = True try: library = self.get_library() except Exception as ex: # pylint=disable:broad-except self.is_loading = False logger.error("Failed to load EGS library: %s", ex) return egs_games = [] for game in library: egs_game = EGSGame.new_from_api(game) egs_game.save() egs_games.append(egs_game) self.is_loading = False return egs_games
def prelaunch(self): def check_shutdown(is_running, times=10): for _ in range(1, times): time.sleep(1) if not is_running(): return True # If using primusrun, shutdown existing Steam first optimus = self.system_config.get('optimus') if optimus != 'off': if is_running(): logger.info("Waiting for Steam shutdown...") shutdown() if not check_shutdown(is_running): logger.info("Steam does not shut down, killing it...") kill() if not check_shutdown(is_running, 5): logger.error("Failed to shut down Steam :(") return False return True
def download(self): """Download DXVK to the local cache""" if self.is_available(): logger.warning("DXVK already available at %s", self.dxvk_path) dxvk_url = self.get_dxvk_download_url() if not dxvk_url: logger.warning("Could not find a release for DXVK %s", self.version) return dxvk_archive_path = os.path.join(self.base_dir, os.path.basename(dxvk_url)) download_file(dxvk_url, dxvk_archive_path, overwrite=True) if not system.path_exists(dxvk_archive_path) or not os.stat( dxvk_archive_path).st_size: logger.error("Failed to download DXVK %s", self.version) return extract_archive(dxvk_archive_path, self.dxvk_path, merge_single=True) os.remove(dxvk_archive_path)
def get_unix_path(self, windows_path): windows_path = windows_path.replace("\\\\", "/") if not self.prefix_path: return drives_path = os.path.join(self.prefix_path, "dosdevices") if not system.path_exists(drives_path): return letter, relpath = windows_path.split(":", 1) relpath = relpath.strip("/") drive_link = os.path.join(drives_path, letter.lower() + ":") try: drive_path = os.readlink(drive_link) except FileNotFoundError: logger.error("Unable to read link for %s", drive_link) return if not os.path.isabs(drive_path): drive_path = os.path.join(drives_path, drive_path) return os.path.join(drive_path, relpath)
def _get_screen_saver_inhibitor(): """Return the appropriate screen saver inhibitor instance. Returns None if the required interface isn't available.""" desktop_environment = get_desktop_environment() if desktop_environment is DesktopEnvironment.MATE: name = "org.mate.ScreenSaver" path = "/" elif desktop_environment is DesktopEnvironment.XFCE: name = "org.xfce.ScreenSaver" path = "/" else: name = "org.freedesktop.ScreenSaver" path = "/org/freedesktop/ScreenSaver" interface = name try: return DBusScreenSaverInhibitor(name, path, interface) except GLib.Error as err: logger.error("Error during creation of DBusScreenSaverInhibitor: %s", err.message) return None
def ensure_discord_connected(self): """Make sure we are actually connected before trying to send requests""" if not self.available(): logger.debug("Discord Rich Presence not available due to lack of pypresence") return logger.debug("Ensuring connected.") if self.presence_connected: logger.debug("Already connected!") else: logger.debug("Creating Presence object.") self.rpc_client = PyPresence(self.client_id) try: logger.debug("Attempting to connect.") self.rpc_client.connect() self.presence_connected = True except Exception as e: logger.error("Unable to reach Discord. Skipping update: %s", e) self.ensure_discord_disconnected() return self.presence_connected
def get_runner_version(self, version=None): """Get the appropriate version for a runner Params: version (str): Optional version to lookup, will return this one if found Returns: dict: Dict containing version, architecture and url for the runner """ logger.info( "Getting runner information for %s%s", self.name, " (version: %s)" % version if version else "", ) request = Request("{}/api/runners/{}".format(settings.SITE_URL, self.name)) runner_info = request.get().json if not runner_info: logger.error("Failed to get runner information") return versions = runner_info.get("versions") or [] arch = system.LINUX_SYSTEM.arch if version: if version.endswith("-i386") or version.endswith("-x86_64"): version, arch = version.rsplit("-", 1) versions = [v for v in versions if v["version"] == version] versions_for_arch = [v for v in versions if v["architecture"] == arch] if len(versions_for_arch) == 1: return versions_for_arch[0] if len(versions_for_arch) > 1: default_version = [v for v in versions_for_arch if v["default"] is True] if default_version: return default_version[0] elif len(versions) == 1 and system.LINUX_SYSTEM.is_64_bit: return versions[0] elif len(versions) > 1 and system.LINUX_SYSTEM.is_64_bit: default_version = [v for v in versions if v["default"] is True] if default_version: return default_version[0] # If we didn't find a proper version yet, return the first available. if len(versions_for_arch) >= 1: return versions_for_arch[0]
def connect(username, password): """Connect to the Lutris API""" credentials = urllib.parse.urlencode( {"username": username, "password": password} ).encode("utf-8") login_url = settings.SITE_URL + "/api/accounts/token" try: request = urllib.request.urlopen(login_url, credentials, 10) except (socket.timeout, urllib.error.URLError) as ex: logger.error("Unable to connect to server (%s): %s", login_url, ex) return False response = json.loads(request.read().decode()) if "token" in response: token = response["token"] with open(API_KEY_FILE_PATH, "w") as token_file: token_file.write(":".join((username, token))) get_user_info() return response["token"] return False
def uninstall_runner_cli(self, runner_name): """ uninstall the runner given in application file located in lutris/gui/application.py provided using lutris -u <runner> """ try: runner_class = import_runner(runner_name) runner = runner_class() except InvalidRunner: logger.error("Failed to import Runner: %s", runner_name) return if not runner.is_installed(): print(f"Runner '{runner_name}' is not installed.") return if runner.can_uninstall(): runner.uninstall() print(f"'{runner_name}' has been uninstalled.") else: print(f"Runner '{runner_name}' cannot be uninstalled.")
def __init__(self, game_id=None): super().__init__() self.id = game_id self.runner = None self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.exit_main_loop = False self.xboxdrv_thread = None game_data = pga.get_game_by_field(game_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")) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.playtime = game_data.get("playtime") or 0.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")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.load_config() self.game_runtime_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") self.timer = Timer() try: self.playtime = float(game_data.get("playtime") or 0.0) except ValueError: logger.error("Invalid playtime value %s", game_data.get("playtime"))
def enable_dll(self, system_dir, arch, dll_path): """Copies dlls to the appropriate destination""" dll = os.path.basename(dll_path) if system.path_exists(dll_path): wine_dll_path = os.path.join(system_dir, dll) if system.path_exists(wine_dll_path): if not self.is_managed_dll( wine_dll_path) and not os.path.islink(wine_dll_path): # Backing up original version (may not be needed) shutil.move(wine_dll_path, wine_dll_path + ".orig") else: os.remove(wine_dll_path) try: os.symlink(dll_path, wine_dll_path) except OSError: logger.error("Failed linking %s to %s", dll_path, wine_dll_path) else: self.disable_dll(system_dir, arch, dll)
def update_discord_rich_presence(self): """Dispatch a request to Discord to update presence""" if int(time.time()) - self.rpc_interval < self.last_rpc: logger.debug("Not enough time since last RPC") return if self.rpc_enabled: self.last_rpc = int(time.time()) if not self.connect(): return try: state_text = "via %s" % self.runner_name if self.show_runner else " " logger.info("Attempting to update Discord status: %s, %s", self.game_name, state_text) self.rpc_client.update(details="Playing %s" % self.game_name, state=state_text, large_image="large_image", large_text="Using Lutris") except PyPresenceException as ex: logger.error("Unable to update Discord: %s", ex)
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 prelaunch(self): super(winesteam, self).prelaunch() def check_shutdown(is_running, times=10): for x in range(1, times + 1): time.sleep(1) if not is_running(): return True # Stop existing winesteam to prevent Wine prefix/version problems if is_running(): logger.info("Waiting for Steam to shutdown...") self.shutdown() if not check_shutdown(is_running): logger.info("Wine Steam does not shut down, killing it...") kill() if not check_shutdown(is_running, 5): logger.error("Failed to shut down Wine Steam :(") return False return True
def force_shutdown(self): """Forces a Steam shutdown, double checking its exit status and raising an error if it cannot be killed""" def has_steam_shutdown(times=10): for __ in range(1, times + 1): time.sleep(1) if not is_running(): return True # Stop existing winesteam to prevent Wine prefix/version problems if is_running(): logger.info("Waiting for Steam to shutdown...") self.shutdown() if not has_steam_shutdown(): logger.info("Forcing Steam shutdown") kill() if not has_steam_shutdown(5): logger.error("Failed to shut down Wine Steam :(")
def launch(self): """Request launching a game. The game may not be installed yet.""" if not self.is_installed: raise RuntimeError("Tried to launch a game that isn't installed") self.load_config() # Reload the config before launching it. if str(self.id) in LOG_BUFFERS: # Reset game logs on each launch LOG_BUFFERS.pop(str(self.id)) if not self.runner: dialogs.ErrorDialog(_("Invalid game configuration: Missing runner")) return if not self.is_launchable(): logger.error("Game is not launchable") return self.state = self.STATE_LAUNCHING self.prelaunch_pids = system.get_running_pid_list() self.emit("game-start") jobs.AsyncCall(self.runner.prelaunch, self.configure_game)
def download(self, slug: str, url: str) -> Optional[str]: """Downloads the banner if not present""" if not url: return cache_path = os.path.join(self.dest_path, self.get_filename(slug)) if system.path_exists(cache_path, exclude_empty=True): return if system.path_exists(cache_path): cache_stats = os.stat(cache_path) # Empty files have a life time between 1 and 2 weeks, retry them after if time.time() - cache_stats.st_mtime < 3600 * 24 * random.choice( range(7, 15)): return cache_path os.unlink(cache_path) try: return download_file(url, cache_path, raise_errors=True) except HTTPError as ex: logger.error(ex) return
def check_driver(): """Report on the currently running driver""" driver_info = {} if drivers.is_nvidia(): driver_info = drivers.get_nvidia_driver_info() # pylint: disable=logging-format-interpolation logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"])) gpus = drivers.get_nvidia_gpu_ids() for gpu_id in gpus: gpu_info = drivers.get_nvidia_gpu_info(gpu_id) logger.info("GPU: %s", gpu_info.get("Model")) elif LINUX_SYSTEM.glxinfo: # pylint: disable=no-member if hasattr(LINUX_SYSTEM.glxinfo, "GLX_MESA_query_renderer"): logger.info( "Running %s Mesa driver %s on %s", LINUX_SYSTEM.glxinfo.opengl_vendor, LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version, LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.device, ) else: logger.warning("glxinfo is not available on your system, unable to detect driver version") for card in drivers.get_gpus(): # pylint: disable=logging-format-interpolation try: logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} ({DRIVER} drivers)".format(**drivers.get_gpu_info(card))) except KeyError: logger.error("Unable to get GPU information from '%s'", card) if drivers.is_outdated(): setting = "hide-outdated-nvidia-driver-warning" if settings.read_setting(setting) != "True": DontShowAgainDialog( setting, _("Your NVIDIA driver is outdated."), secondary_message=_("You are currently running driver %s which does not " "fully support all features for Vulkan and DXVK games.\n" "Please upgrade your driver as described in our " "<a href='https://github.com/lutris/lutris/wiki/Installing-drivers'>" "installation guide</a>") % driver_info["nvrm"]["version"], )
def __init__( self, url, timeout=30, stop_request=None, headers=None, cookies=None, ): if not url: raise ValueError("An URL is required!") if url == "None": raise ValueError("You'd better stop that right now.") if url.startswith("//"): url = "https:" + url if url.startswith("/"): logger.error("Stop using relative URLs!: %s", url) url = SITE_URL + url self.url = url self.status_code = None self.content = b"" self.timeout = timeout self.stop_request = stop_request self.buffer_size = 1024 * 1024 # Bytes self.total_size = None self.downloaded_size = 0 self.headers = {"User-Agent": self.user_agent} self.response_headers = None self.info = None if headers is None: headers = {} if not isinstance(headers, dict): raise TypeError("HTTP headers needs to be a dict ({})".format(headers)) self.headers.update(headers) if cookies: cookie_processor = urllib.request.HTTPCookieProcessor(cookies) self.opener = urllib.request.build_opener(cookie_processor) else: self.opener = None
def __init__(self, game_id=None): super().__init__() self.id = game_id # pylint: disable=invalid-name self.runner = None self.config = None # Load attributes from database game_data = pga.get_game_by_field(game_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.game_config_id = game_data.get("configpath") or "" self.is_installed = bool(game_data.get("installed") and self.game_config_id) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.steamid = game_data.get("steamid") or "" self.has_custom_banner = bool(game_data.get("has_custom_banner")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.discord_presence = DiscordPresence() try: self.playtime = float(game_data.get("playtime") or 0.0) except ValueError: logger.error("Invalid playtime value %s", game_data.get("playtime")) self.playtime = 0.0 if self.game_config_id: self.load_config() self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.killswitch = None self.state = self.STATE_IDLE self.game_runtime_config = {} self.resolution_changed = False self.compositor_disabled = False self.stop_compositor = self.start_compositor = "" self.original_outputs = None self._log_buffer = None self.timer = Timer()
def get_outputs(): """Return list of namedtuples containing output 'name', 'geometry', 'rotation' and whether it is the 'primary' display.""" outputs = [] vid_modes = _get_vidmodes() position = None rotate = None primary = None name = None if not vid_modes: logger.error("xrandr didn't return anything") return [] for line in vid_modes: if "connected" in line: primary = "primary" in line if primary: name, _, _, geometry, rotate, *_ = line.split() else: name, _, geometry, rotate, *_ = line.split() if geometry.startswith("("): # Screen turned off, no geometry continue if rotate.startswith("("): # Screen not rotated, no need to include rotate = "normal" _, x_pos, y_pos = geometry.split("+") position = "{x_pos}x{y_pos}".format(x_pos=x_pos, y_pos=y_pos) elif "*" in line: mode, *framerates = line.split() for number in framerates: if "*" in number: hertz = number[:-2] outputs.append( Output( name=name, mode=mode, position=position, rotation=rotate, primary=primary, rate=hertz, ) ) break return outputs
def get_api_games(game_slugs=None, page="1", query_type="games"): """Return all games from the Lutris API matching the given game slugs""" response_data = get_game_api_page(game_slugs, page=page, query_type=query_type) if not response_data: return [] results = response_data.get("results", []) while response_data.get("next"): page_match = re.search(r"page=(\d+)", response_data["next"]) if page_match: next_page = page_match.group(1) else: logger.error("No page found in %s", response_data["next"]) break response_data = get_game_api_page(game_slugs, page=next_page, query_type=query_type) if not response_data.get("results"): logger.warning("Unable to get response for page %s", next_page) break else: results += response_data.get("results") return results
def get_display_manager(): """Return the appropriate display manager instance. Defaults to Mutter if available. This is the only one to support Wayland. """ if DBUS_AVAILABLE: try: return MutterDisplayManager() except DBusException as ex: logger.debug("Mutter DBus service not reachable: %s", ex) except Exception as ex: # pylint: disable=broad-except logger.exception( "Failed to instanciate MutterDisplayConfig. Please report with exception: %s", ex) else: logger.error( "DBus is not available, lutris was not properly installed.") try: return DisplayManager() except (GLib.Error, NoScreenDetected): return LegacyDisplayManager()
def update_gui(result, error): if result: added_ids, updated_ids = result # sqlite limits the number of query parameters to 999, to # bypass that limitation, divide the query in chunks page_size = 999 added_games = chain.from_iterable([ pga.get_games_where( id__in=list(added_ids)[p * page_size:p * page_size + page_size]) for p in range(math.ceil(len(added_ids) / page_size)) ]) self.game_list += added_games self.view.populate_games(added_games) self.switch_splash_screen() GLib.idle_add(self.update_existing_games, added_ids, updated_ids, True) else: logger.error("No results returned when syncing the library")
def download(self): """Download DXVK to the local cache""" dxvk_url = self.base_url.format(self.version, self.version) if self.is_available(): logger.warning("DXVK already available at %s", self.dxvk_path) dxvk_archive_path = os.path.join(self.base_dir, os.path.basename(dxvk_url)) downloader = Downloader(dxvk_url, dxvk_archive_path) downloader.start() while downloader.check_progress() < 1: time.sleep(0.3) if not os.path.exists(dxvk_archive_path): logger.error("DXVK %s not downloaded") return if os.stat(dxvk_archive_path).st_size: extract_archive(dxvk_archive_path, self.dxvk_path, merge_single=True) os.remove(dxvk_archive_path) else: os.remove(dxvk_archive_path) raise UnavailableDXVKVersion("Failed to download DXVK %s" % self.version)
def parse(self, line): """Parse a registry line, populating meta and subkeys""" if len(line) < 4: # Line is too short, nothing to parse return if line.startswith("#"): self.add_meta(line) elif line.startswith('"'): try: key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1) except ValueError as ex: logger.error("Unable to parse line %s", line) logger.exception(ex) return key = key[1:-1] self.subkeys[key] = value elif line.startswith("@"): key, value = line.split("=", 1) self.subkeys["default"] = value
def get_pixbuf_for_game(game_slug, icon_type, is_installed=True): if icon_type in ("banner", "banner_small"): default_icon_path = DEFAULT_BANNER icon_path = datapath.get_banner_path(game_slug) elif icon_type in ("icon", "icon_small"): default_icon_path = DEFAULT_ICON icon_path = datapath.get_icon_path(game_slug) else: logger.error("Invalid icon type '%s'", icon_type) return size = IMAGE_SIZES[icon_type] pixbuf = get_pixbuf(icon_path, default_icon_path, size) if not is_installed: transparent_pixbuf = get_overlay(size).copy() pixbuf.composite(transparent_pixbuf, 0, 0, size[0], size[1], 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 100) return transparent_pixbuf return pixbuf
def query_download_links(self, download): """Convert files from the GOG API to a format compatible with lutris installers""" download_links = [] for game_file in download.get("files", []): downlink = game_file.get("downlink") if not downlink: logger.error("No download information for %s", game_file) continue download_info = self.get_download_info(downlink) for field in ('checksum', 'downlink'): download_links.append({ "name": download.get("name", ""), "os": download.get("os", ""), "type": download.get("type", ""), "total_size": download.get("total_size", 0), "id": str(game_file["id"]), "url": download_info[field], "filename": download_info[field + "_filename"] }) return download_links