From 80bc82df55082cc48cfa714eee9257bd1744756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 10 Nov 2017 11:07:02 +0100 Subject: [PATCH] Protect against broken packages in python env As seen in https://groups.google.com/forum/#!msg/octoprint/DyXdqhR0U7c/kKMUsMmIBgAJ a broken entry_points.txt in some arbitrary Python package installed in the same python envrionment as OctoPrint can make our whole plugin detection fail and hence interrupt regular server startup. This adds better protection against such cases. --- src/octoprint/plugin/core.py | 184 ++++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 77 deletions(-) diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index d8a58e9e..4231b56c 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -551,53 +551,69 @@ class PluginManager(object): result = OrderedDict() if self.plugin_folders: - result.update(self._find_plugins_from_folders(self.plugin_folders, existing, ignored_uninstalled=ignore_uninstalled)) + try: + result.update(self._find_plugins_from_folders(self.plugin_folders, + existing, + ignored_uninstalled=ignore_uninstalled)) + except: + self.logger.exception("Error fetching plugins from folders") if self.plugin_entry_points: existing.update(result) - result.update(self._find_plugins_from_entry_points(self.plugin_entry_points, existing, ignore_uninstalled=ignore_uninstalled)) + try: + result.update(self._find_plugins_from_entry_points(self.plugin_entry_points, + existing, + ignore_uninstalled=ignore_uninstalled)) + except: + self.logger.exception("Error fetching plugins from entry points") return result def _find_plugins_from_folders(self, folders, existing, ignored_uninstalled=True): result = OrderedDict() for folder in folders: - flagged_readonly = False - if isinstance(folder, (list, tuple)): - if len(folder) == 2: - folder, flagged_readonly = folder - else: - continue - actual_readonly = not os.access(folder, os.W_OK) - - if not os.path.exists(folder): - self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder)) - continue - - for entry in scandir(folder): - if entry.is_dir() and os.path.isfile(os.path.join(entry.path, "__init__.py")): - key = entry.name - elif entry.is_file() and entry.name.endswith(".py"): - key = entry.name[:-3] # strip off the .py extension - if key.startswith("__"): - # might be an __init__.py in our plugins folder, or something else we don't want - # to handle + try: + flagged_readonly = False + if isinstance(folder, (list, tuple)): + if len(folder) == 2: + folder, flagged_readonly = folder + else: continue - else: + actual_readonly = not os.access(folder, os.W_OK) + + if not os.path.exists(folder): + self.logger.warn("Plugin folder {folder} could not be found, skipping it".format(folder=folder)) continue - if key in existing or key in result or (ignored_uninstalled and key in self.marked_plugins["uninstalled"]): - # plugin is already defined, ignore it - continue + for entry in scandir(folder): + try: + if entry.is_dir() and os.path.isfile(os.path.join(entry.path, "__init__.py")): + key = entry.name + elif entry.is_file() and entry.name.endswith(".py"): + key = entry.name[:-3] # strip off the .py extension + if key.startswith("__"): + # might be an __init__.py in our plugins folder, or something else we don't want + # to handle + continue + else: + continue - plugin = self._import_plugin_from_module(key, folder=folder) - if plugin: - plugin.origin = FolderOrigin("folder", folder) - plugin.managable = not flagged_readonly and not actual_readonly - plugin.bundled = flagged_readonly + if key in existing or key in result or (ignored_uninstalled and key in self.marked_plugins["uninstalled"]): + # plugin is already defined, ignore it + continue - plugin.enabled = False + plugin = self._import_plugin_from_module(key, folder=folder) + if plugin: + plugin.origin = FolderOrigin("folder", folder) + plugin.managable = not flagged_readonly and not actual_readonly + plugin.bundled = flagged_readonly - result[key] = plugin + plugin.enabled = False + + result[key] = plugin + except: + self.logger.exception("Error processing folder entry {!r} from folder {}".format(entry, folder)) + except: + self.logger.exception("Error processing folder {}".format(folder)) return result @@ -619,51 +635,65 @@ class PluginManager(object): if not isinstance(groups, (list, tuple)): groups = [groups] + def wrapped(gen): + # to protect against some issues in installed packages that make iteration over entry points + # fall on its face - e.g. https://groups.google.com/forum/#!msg/octoprint/DyXdqhR0U7c/kKMUsMmIBgAJ + try: + yield next(gen) + except StopIteration: + raise + except: + self.logger.exception("Something went wrong while processing the entry points of a package in the " + "Python environment - broken entry_points.txt in some package?") + for group in groups: - for entry_point in working_set.iter_entry_points(group=group, name=None): - key = entry_point.name - module_name = entry_point.module_name - version = entry_point.dist.version - - if key in existing or key in result or (ignore_uninstalled and key in self.marked_plugins["uninstalled"]): - # plugin is already defined or marked as uninstalled, ignore it - continue - - kwargs = dict(module_name=module_name, version=version) - package_name = None + for entry_point in wrapped(working_set.iter_entry_points(group=group, name=None)): try: - module_pkginfo = InstalledEntryPoint(entry_point) + key = entry_point.name + module_name = entry_point.module_name + version = entry_point.dist.version + + if key in existing or key in result or (ignore_uninstalled and key in self.marked_plugins["uninstalled"]): + # plugin is already defined or marked as uninstalled, ignore it + continue + + kwargs = dict(module_name=module_name, version=version) + package_name = None + try: + module_pkginfo = InstalledEntryPoint(entry_point) + except: + self.logger.exception("Something went wrong while retrieving package info data for module %s" % module_name) + else: + kwargs.update(dict( + name=module_pkginfo.name, + summary=module_pkginfo.summary, + author=module_pkginfo.author, + url=module_pkginfo.home_page, + license=module_pkginfo.license + )) + package_name = module_pkginfo.name + + plugin = self._import_plugin_from_module(key, **kwargs) + if plugin: + plugin.origin = EntryPointOrigin("entry_point", group, module_name, package_name, version) + + # plugin is manageable if its location is writable and OctoPrint + # is either not running from a virtual env or the plugin is + # installed in that virtual env - the virtual env's pip will not + # allow us to uninstall stuff that is installed outside + # of the virtual env, so this check is necessary + plugin.managable = os.access(plugin.location, os.W_OK) \ + and (not self._python_virtual_env + or is_sub_path_of(plugin.location, self._python_prefix) + or is_editable_install(self._python_install_dir, + package_name, + module_name, + plugin.location)) + + plugin.enabled = False + result[key] = plugin except: - self.logger.exception("Something went wrong while retrieving package info data for module %s" % module_name) - else: - kwargs.update(dict( - name=module_pkginfo.name, - summary=module_pkginfo.summary, - author=module_pkginfo.author, - url=module_pkginfo.home_page, - license=module_pkginfo.license - )) - package_name = module_pkginfo.name - - plugin = self._import_plugin_from_module(key, **kwargs) - if plugin: - plugin.origin = EntryPointOrigin("entry_point", group, module_name, package_name, version) - - # plugin is manageable if its location is writable and OctoPrint - # is either not running from a virtual env or the plugin is - # installed in that virtual env - the virtual env's pip will not - # allow us to uninstall stuff that is installed outside - # of the virtual env, so this check is necessary - plugin.managable = os.access(plugin.location, os.W_OK) \ - and (not self._python_virtual_env - or is_sub_path_of(plugin.location, self._python_prefix) - or is_editable_install(self._python_install_dir, - package_name, - module_name, - plugin.location)) - - plugin.enabled = False - result[key] = plugin + self.logger.exception("Error processing entry point {!r} for group {}".format(entry_point, group)) return result @@ -717,10 +747,10 @@ class PluginManager(object): entry_key, entry_version = entry return entry_key == key and entry_version == version return False - + return any(map(lambda entry: matches_plugin(entry), self.plugin_blacklist)) - + def reload_plugins(self, startup=False, initialize_implementations=True, force_reload=None): self.logger.info("Loading plugins from {folders} and installed plugin packages...".format( folders=", ".join(map(lambda x: x[0] if isinstance(x, tuple) else str(x), self.plugin_folders))