Beispiel #1
0
 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))
Beispiel #2
0
    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
Beispiel #3
0
    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")
Beispiel #4
0
    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)
Beispiel #5
0
    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"
Beispiel #6
0
    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)
Beispiel #7
0
    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'
Beispiel #8
0
 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)
Beispiel #9
0
    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)
Beispiel #10
0
    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)