def on_steam_game_installed(_data, error): """Callback for Steam game installer, mostly for error handling since install progress is handled by _monitor_steam_game_install""" if error: raise ScriptingError(str(error))
def __init__(self, installer, parent): self.error = None self.errors = [] self.target_path = None self.parent = parent self.game_dir_created = False # Whether a game folder was created during the install self.game_files = {} self.game_disc = None self.cancelled = False self.abort_current_task = None self.user_inputs = [] self.steam_data = {} self.gog_data = {} self.script = installer.get("script") if not self.script: raise ScriptingError("This installer doesn't have a 'script' section") self.script_pretty = json.dumps(self.script, indent=4) self.install_start_time = None # Time of the start of the install self.steam_poll = None # Reference to the Steam poller that checks if games are downloaded self.current_command = None # Current installer command when iterating through them self.current_file_id = None # Current file when downloading / gathering files self.runners_to_install = [] self.prev_states = [] # Previous states for the Steam installer self.version = installer["version"] self.slug = installer["slug"] self.year = installer.get("year") self.runner = installer["runner"] self.game_name = self.script.get("custom-name") or installer["name"] self.game_slug = installer["game_slug"] self.steamid = installer.get("steamid") self.gogid = installer.get("gogid") if not self.is_valid(): raise ScriptingError( "Invalid script: \n{}".format("\n".join(self.errors)), self.script ) self.files = [ InstallerFile(self.game_slug, file_id, file_meta) for file_desc in self.script.get("files", []) for file_id, file_meta in file_desc.items() ] self.requires = self.script.get("requires") self.extends = self.script.get("extends") self.current_resolution = DISPLAY_MANAGER.get_current_resolution() self._check_binary_dependencies() self._check_dependency() if self.creates_game_folder: self.target_path = self.get_default_target() # If the game is in the library and uninstalled, the first installation # updates it existing_game = pga.get_game_by_field(self.game_slug, "slug") if existing_game and not existing_game["installed"]: self.game_id = existing_game["id"] else: self.game_id = None
def _save_game(self): # pylint: disable=too-many-branches """Write the game configuration in the DB and config file. This needs to be unfucked """ if self.extends: logger.info( "This is an extension to %s, not creating a new game entry", self.extends, ) return configpath = make_game_config_id(self.slug) config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath) if self.requires: # Load the base game config required_game = pga.get_game_by_field(self.requires, field="installer_slug") base_config = LutrisConfig( runner_slug=self.runner, game_config_id=required_game["configpath"]) config = base_config.game_level else: config = {"game": {}} self.game_id = pga.add_or_update( name=self.game_name, runner=self.runner, slug=self.game_slug, directory=self.target_path, installed=1, installer_slug=self.slug, parent_slug=self.requires, year=self.year, steamid=self.steamid, configpath=configpath, id=self.game_id, ) game = Game(self.game_id) game.save() logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id) # Config update if "system" in self.script: config["system"] = self._substitute_config(self.script["system"]) if self.runner in self.script and self.script[self.runner]: config[self.runner] = self._substitute_config( self.script[self.runner]) # Game options such as exe or main_file can be added at the root of the # script as a shortcut, this integrates them into the game config # properly launcher, launcher_value = _get_game_launcher(self.script) if isinstance(launcher_value, list): game_files = [] for game_file in launcher_value: if game_file in self.game_files: game_files.append(self.game_files[game_file]) else: game_files.append(game_file) config["game"][launcher] = game_files elif launcher_value: if launcher_value in self.game_files: launcher_value = self.game_files[launcher_value] elif self.target_path and os.path.exists( os.path.join(self.target_path, launcher_value)): launcher_value = os.path.join(self.target_path, launcher_value) config["game"][launcher] = launcher_value if "game" in self.script: try: config["game"].update(self.script["game"]) except ValueError: raise ScriptingError("Invalid 'game' section", self.script["game"]) config["game"] = self._substitute_config(config["game"]) yaml_config = yaml.safe_dump(config, default_flow_style=False) with open(config_filename, "w") as config_file: config_file.write(yaml_config) game.emit("game-installed")
def _download_file(self, game_file): """Download a file referenced in the installer script. Game files can be either a string, containing the location of the file to fetch or a dict with the following keys: - url : location of file, if not present, filename will be used this should be the case for local files. - filename : force destination filename when url is present or path of local file. """ # Setup file_id, file_uri and local filename file_id = list(game_file.keys())[0] if isinstance(game_file[file_id], dict): filename = game_file[file_id]['filename'] file_uri = game_file[file_id]['url'] referer = game_file[file_id].get('referer') else: file_uri = game_file[file_id] filename = os.path.basename(file_uri) referer = None if file_uri.startswith("/"): file_uri = "file://" + file_uri elif file_uri.startswith(("$WINESTEAM", "$STEAM")): # Download Steam data self._download_steam_data(file_uri, file_id) return if not filename: raise ScriptingError( "No filename provided, please provide 'url' and 'filename' parameters in the script" ) # Check for file availability in PGA pga_uri = pga.check_for_file(self.game_slug, file_id) if pga_uri: file_uri = pga_uri # Setup destination path dest_file = os.path.join(self.cache_path, filename) logger.debug("Downloading [%s]: %s to %s", file_id, file_uri, dest_file) if file_uri.startswith("N/A"): # Ask the user where the file is located parts = file_uri.split(":", 1) if len(parts) == 2: message = parts[1] else: message = "Please select file '%s'" % file_id self.current_file_id = file_id self.parent.ask_user_for_file(message) return if os.path.exists(dest_file): os.remove(dest_file) # Change parent's status self.parent.set_status('') self.game_files[file_id] = dest_file self.parent.start_download(file_uri, dest_file, referer=referer)
def execute(self, data): """Run an executable file.""" args = [] terminal = None working_dir = None env = {} if isinstance(data, dict): self._check_required_params([("file", "command")], data, "execute") if "command" in data and "file" in data: raise ScriptingError( "Parameters file and command can't be used " "at the same time for the execute command", data, ) file_ref = data.get("file", "") command = data.get("command", "") args_string = data.get("args", "") for arg in shlex.split(args_string): args.append(self._substitute(arg)) terminal = data.get("terminal") working_dir = data.get("working_dir") if not data.get("disable_runtime"): # Possibly need to handle prefer_system_libs here env.update(runtime.get_env()) # Loading environment variables set in the script env.update(self.script_env) # Environment variables can also be passed to the execute command local_env = data.get("env") or {} env.update({ key: self._substitute(value) for key, value in local_env.items() }) include_processes = shlex.split(data.get("include_processes", "")) exclude_processes = shlex.split(data.get("exclude_processes", "")) elif isinstance(data, str): command = data include_processes = [] exclude_processes = [] else: raise ScriptingError("No parameters supplied to execute command.", data) if command: file_ref = "bash" args = ["-c", self._get_file(command.strip())] include_processes.append("bash") else: # Determine whether 'file' value is a file id or a path file_ref = self._get_file(file_ref) exec_path = system.find_executable(file_ref) if not exec_path: raise ScriptingError("Unable to find executable %s" % file_ref) if not os.access(exec_path, os.X_OK): logger.warning("Making %s executable", exec_path) self.chmodx(exec_path) if terminal: terminal = system.get_default_terminal() if not working_dir or not os.path.exists(working_dir): working_dir = self.target_path command = MonitoredCommand( [exec_path] + args, env=env, term=terminal, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes, ) command.start() GLib.idle_add(self.parent.attach_logger, command) self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command) return "STOP"
def choose_installer(self): """Stage where we choose an install script.""" self.title_label.set_markup('<b>Select which version to install</b>') self.installer_choice_box = Gtk.VBox() self.installer_choice = 0 radio_group = None # Build list for index, script in enumerate(self.scripts): for item in ['description', 'notes']: script[item] = script.get(item) or '' for item in ['runner', 'version']: if item not in script: logger.error("Invalid script: %s", script) raise ScriptingError( 'Missing field "%s" in install script' % item) runner = script['runner'] version = script['version'] label = "{} ({})".format(version, runner) btn = Gtk.RadioButton.new_with_label_from_widget( radio_group, label) btn.connect('toggled', self.on_installer_toggled, index) self.installer_choice_box.pack_start(btn, False, False, 10) if not radio_group: radio_group = btn def _create_label(text): label = Gtk.Label() label.set_max_width_chars(60) label.set_line_wrap(True) label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR) label.set_alignment(0, .5) label.set_margin_left(50) label.set_margin_right(50) label.set_markup(self._escape_text(text)) return label self.description_label = _create_label("<b>{}</b>".format( self.scripts[0]['description'])) self.installer_choice_box.pack_start(self.description_label, True, True, 10) self.notes_label = _create_label("{}".format(self.scripts[0]['notes'])) notes_scrolled_area = Gtk.ScrolledWindow() try: notes_scrolled_area.set_propagate_natural_height(True) except AttributeError: logger.debug("set_propagate_natural_height not available") notes_scrolled_area.set_min_content_height(100) notes_scrolled_area.add(self.notes_label) self.installer_choice_box.pack_start(notes_scrolled_area, True, True, 10) self.widget_box.pack_start(self.installer_choice_box, False, False, 10) self.installer_choice_box.show_all() self.continue_button.grab_focus() self.continue_button.show() self.continue_handler = self.continue_button.connect( 'clicked', self.on_installer_selected)
def execute(self, data): """Run an executable file.""" args = [] terminal = None working_dir = None env = {} if isinstance(data, dict): if 'command' not in data and 'file' not in data: raise ScriptingError( 'Parameter file or command is mandatory ' 'for the execute command', data) elif 'command' in data and 'file' in data: raise ScriptingError( 'Parameters file and command can\'t be ' 'used at the same time for the execute ' 'command', data) file_ref = data.get('file', '') command = data.get('command', '') args_string = data.get('args', '') for arg in shlex.split(args_string): args.append(self._substitute(arg)) terminal = data.get('terminal') working_dir = data.get('working_dir') if not data.get('disable_runtime', False): env.update(runtime.get_env()) userenv = data.get('env', {}) for key in userenv: v = userenv[key] userenv[key] = self._get_file(v) or self._substitute(v) env.update(userenv) include_processes = shlex.split(data.get('include_processes', '')) exclude_processes = shlex.split(data.get('exclude_processes', '')) elif isinstance(data, str): command = data include_processes = [] exclude_processes = [] else: raise ScriptingError('No parameters supplied to execute command.', data) if command: command = command.strip() command = self._get_file(command) or self._substitute(command) file_ref = 'bash' args = ['-c', command] include_processes.append('bash') else: # Determine whether 'file' value is a file id or a path file_ref = self._get_file(file_ref) or self._substitute(file_ref) exec_path = system.find_executable(file_ref) if not exec_path: raise ScriptingError("Unable to find executable %s" % file_ref) if not os.access(exec_path, os.X_OK): self.chmodx(exec_path) if terminal: terminal = system.get_default_terminal() if not working_dir or not os.path.exists(working_dir): working_dir = self.target_path command = [exec_path] + args logger.debug("Executing %s" % command) thread = LutrisThread(command, env=env, term=terminal, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes) thread.start() GLib.idle_add(self.parent.attach_logger, thread) self.heartbeat = GLib.timeout_add(1000, self._monitor_task, thread) return 'STOP'
def chmodx(self, filename): """Make filename executable""" filename = self._substitute(filename) if not system.path_exists(filename): raise ScriptingError("Invalid file '%s'. Can't make it executable" % filename) system.make_executable(filename)
def write_config(self): """Write the game configuration in the DB and config file""" if self.extends: logger.info( "This is an extension to %s, not creating a new game entry", self.extends, ) return configpath = make_game_config_id(self.slug) config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath) if self.requires: # Load the base game config required_game = pga.get_game_by_field(self.requires, field="installer_slug") base_config = LutrisConfig( runner_slug=self.runner, game_config_id=required_game["configpath"]) config = base_config.game_level else: config = {"game": {}} self.game_id = pga.add_or_update( name=self.game_name, runner=self.runner, slug=self.game_slug, directory=self.interpreter.target_path, installed=1, installer_slug=self.slug, parent_slug=self.requires, year=self.year, steamid=self.steamid, configpath=configpath, id=self.game_id, ) game = Game(self.game_id) game.save() logger.debug("Saved game entry %s (%d)", self.game_slug, self.game_id) # Config update if "system" in self.script: config["system"] = self._substitute_config(self.script["system"]) if self.runner in self.script and self.script[self.runner]: config[self.runner] = self._substitute_config( self.script[self.runner]) launcher, launcher_config = self.get_game_launcher_config( self.interpreter.game_files) if launcher: config["game"][launcher] = launcher_config if "game" in self.script: try: config["game"].update(self.script["game"]) except ValueError: raise ScriptingError("Invalid 'game' section", self.script["game"]) config["game"] = self._substitute_config(config["game"]) yaml_config = yaml.safe_dump(config, default_flow_style=False) with open(config_filename, "w") as config_file: config_file.write(yaml_config)
def _download_file(self, game_file): """Download a file referenced in the installer script. KILL IT WITH FIRE!!! This method is a mess. Game files can be either a string, containing the location of the file to fetch or a dict with the following keys: - url : location of file, if not present, filename will be used this should be the case for local files. - filename : force destination filename when url is present or path of local file. """ if not isinstance(game_file, dict): raise ScriptingError("Invalid file, check the installer script", game_file) # Setup file_id, file_uri and local filename file_id = list(game_file.keys())[0] file_meta = game_file[file_id] 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)) file_uri = file_meta["url"] filename = file_meta["filename"] referer = file_meta.get("referer") checksum = file_meta.get("checksum") else: file_uri = file_meta filename = os.path.basename(file_uri) referer = None checksum = None if file_uri.startswith("/"): file_uri = "file://" + file_uri elif file_uri.startswith(("$WINESTEAM", "$STEAM")): # Download Steam data self._download_steam_data(file_uri, file_id) return if not filename: raise ScriptingError( "No filename provided, please provide 'url' and 'filename' parameters in the script" ) # Check for file availability in PGA pga_uri = pga.check_for_file(self.game_slug, file_id) if pga_uri: file_uri = pga_uri # Setup destination path dest_file = os.path.join(self.cache_path, filename) logger.debug("Downloading [%s]: %s to %s", file_id, file_uri, dest_file) if file_uri.startswith("N/A"): # Ask the user where the file is located parts = file_uri.split(":", 1) if len(parts) == 2: message = parts[1] else: message = "Please select file '%s'" % file_id self.current_file_id = file_id self.parent.ask_user_for_file(message) return if os.path.exists(dest_file): os.remove(dest_file) # Change parent's status self.parent.set_status("") self.game_files[file_id] = dest_file if checksum: self.parent.start_download( file_uri, dest_file, lambda *args: self.check_hash(checksum, dest_file, file_uri), referer=referer) else: self.parent.start_download(file_uri, dest_file, referer=referer)