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.
This commit is contained in:
Gina Häußge 2017-11-10 11:07:02 +01:00
parent 651a8f315b
commit 80bc82df55

View file

@ -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))