diff --git a/MANIFEST.in b/MANIFEST.in index abffeac2..15833310 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ recursive-include src/octoprint/static * recursive-include src/octoprint/templates * recursive-include src/octoprint/plugins * recursive-include src/octoprint/translations * +include src/octoprint/util/piptestballoon/setup.py include versioneer.py include src/octoprint/_version.py include AUTHORS.md diff --git a/setup.py b/setup.py index 3633a628..ec24b83f 100644 --- a/setup.py +++ b/setup.py @@ -149,7 +149,9 @@ def params(): "": "src", } package_data = { - "octoprint": octoprint_setuptools.package_data_dirs('src/octoprint', ['static', 'templates', 'plugins', 'translations']) + "octoprint": octoprint_setuptools.package_data_dirs('src/octoprint', + ['static', 'templates', 'plugins', 'translations']) + + ['util/piptestballoon/setup.py'] } include_package_data = True diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 30577575..a5c223f3 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -133,6 +133,7 @@ class PluginInfo(object): self.enabled = True self.bundled = False self.loaded = False + self.managable = True self._name = name self._version = version @@ -475,8 +476,25 @@ class PluginManager(object): self.marked_plugins = defaultdict(list) + self._python_install_dir = None + self._python_virtual_env = False + self._detect_python_environment() + self.reload_plugins(startup=True, initialize_implementations=False) + def _detect_python_environment(self): + from distutils.command.install import install as cmd_install + from distutils.dist import Distribution + import sys + + cmd = cmd_install(Distribution()) + cmd.finalize_options() + + self._python_install_dir = cmd.install_lib + self._python_prefix = sys.prefix + self._python_virtual_env = hasattr(sys, "real_prefix") \ + or (hasattr(sys, "base_prefix") and sys.prefix != sys.base_prefix) + @property def plugins(self): plugins = dict(self.enabled_plugins) @@ -503,12 +521,13 @@ class PluginManager(object): result = dict() for folder in folders: - readonly = False + flagged_readonly = False if isinstance(folder, (list, tuple)): if len(folder) == 2: - folder, readonly = folder + 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)) @@ -531,8 +550,8 @@ class PluginManager(object): plugin = self._import_plugin_from_module(key, folder=folder) if plugin: plugin.origin = FolderOrigin("folder", folder) - if readonly: - plugin.bundled = True + plugin.managable = not flagged_readonly and not actual_readonly + plugin.bundled = flagged_readonly plugin.enabled = False @@ -543,9 +562,18 @@ class PluginManager(object): def _find_plugins_from_entry_points(self, groups, existing, ignore_uninstalled=True): result = dict() - # let's make sure we have a current working set + # let's make sure we have a current working set ... working_set = pkg_resources.WorkingSet() + # ... including the user's site packages + import site + import sys + if site.ENABLE_USER_SITE: + if not site.USER_SITE in working_set.entries: + working_set.add_entry(site.USER_SITE) + if not site.USER_SITE in sys.path: + site.addsitedir(site.USER_SITE) + if not isinstance(groups, (list, tuple)): groups = [groups] @@ -578,6 +606,16 @@ class PluginManager(object): 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 plugin.location.startswith(self._python_prefix)) + plugin.enabled = False result[key] = plugin @@ -648,15 +686,24 @@ class PluginManager(object): hooks=sum(map(lambda x: len(x), self.plugin_hooks.values())) )) - def mark_plugin(self, name, uninstalled=None): + def mark_plugin(self, name, **kwargs): if not name in self.plugins: - self.logger.warn("Trying to mark an unknown plugin {name}".format(**locals())) + self.logger.debug("Trying to mark an unknown plugin {name}".format(**locals())) - if uninstalled is not None: - if uninstalled and not name in self.marked_plugins["uninstalled"]: - self.marked_plugins["uninstalled"].append(name) - elif not uninstalled and name in self.marked_plugins["uninstalled"]: - self.marked_plugins["uninstalled"].remove(name) + for key, value in kwargs.items(): + if value is None: + continue + + if value and not name in self.marked_plugins[key]: + self.marked_plugins[key].append(name) + elif not value and name in self.marked_plugins[key]: + self.marked_plugins[key].remove(name) + + def is_plugin_marked(self, name, key): + if not name in self.plugins: + return False + + return name in self.marked_plugins[key] def load_plugin(self, name, plugin=None, startup=False, initialize_implementation=True): if not name in self.plugins: @@ -886,7 +933,7 @@ class PluginManager(object): additional_pre_inits=additional_pre_inits, additional_post_inits=additional_post_inits) - self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations))) + self.logger.info("Initialized {count} plugin implementation(s)".format(count=len(self.plugin_implementations))) def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None): if plugin.implementation is None: @@ -980,15 +1027,13 @@ class PluginManager(object): self.logger.info("No plugins available") else: self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join( - sorted( - map(lambda x: "| " + x.long_str(show_bundled=show_bundled, - bundled_strs=bundled_str, - show_location=show_location, - location_str=location_str, - show_enabled=show_enabled, - enabled_strs=enabled_str), - self.enabled_plugins.values()) - ) + map(lambda x: "| " + x.long_str(show_bundled=show_bundled, + bundled_strs=bundled_str, + show_location=show_location, + location_str=location_str, + show_enabled=show_enabled, + enabled_strs=enabled_str), + sorted(self.plugins.values(), key=lambda x: str(x).lower())) ))) def get_plugin(self, identifier, require_enabled=True): diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 7b5679b7..069e8aff 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -52,7 +52,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._repository_cache_path = os.path.join(self.get_plugin_data_folder(), "plugins.json") self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 - self._pip_caller = PipCaller(configured=self._settings.get(["pip"])) + self._pip_caller = PipCaller(configured=self._settings.get(["pip"]), + force_user=self._settings.get_boolean(["pip_force_user"])) self._pip_caller.on_log_call = self._log_call self._pip_caller.on_log_stdout = self._log_stdout self._pip_caller.on_log_stderr = self._log_stderr @@ -84,6 +85,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, repository_ttl=24*60, pip=None, pip_args=None, + pip_force_user=False, dependency_links=False, hidden=[] ) @@ -94,6 +96,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, new_pip = self._settings.get(["pip"]) self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 + self._pip_caller.force_user = self._settings.get_boolean(["pip_force_user"]) if old_pip != new_pip: self._pip_caller.configured = new_pip try: @@ -188,8 +191,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, return jsonify(plugins=self._get_plugins(), repository=dict( - available=self._repository_available, - plugins=self._repository_plugins + available=self._repository_available, + plugins=self._repository_plugins ), os=self._get_os(), octoprint=self._get_octoprint_version_string(), @@ -197,7 +200,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, available=self._pip_caller.available, command=self._pip_caller.command, version=self._pip_caller.version_string, + install_dir=self._pip_caller.install_dir, use_sudo=self._pip_caller.use_sudo, + use_user=self._pip_caller.use_user, + virtual_env=self._pip_caller.virtual_env, additional_args=self._settings.get(["pip_args"]) )) @@ -331,12 +337,15 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._send_result_notification("install", result) return jsonify(result) - self._plugin_manager.mark_plugin(new_plugin_key, uninstalled=False) self._plugin_manager.reload_plugins() - needs_restart = self._plugin_manager.is_restart_needing_plugin(new_plugin) or new_plugin_key in all_plugins_before or reinstall is not None needs_refresh = new_plugin.implementation and isinstance(new_plugin.implementation, octoprint.plugin.ReloadNeedingPlugin) + is_reinstall = self._plugin_manager.is_plugin_marked(new_plugin_key, "uninstalled") + self._plugin_manager.mark_plugin(new_plugin_key, + uninstalled=False, + installed=not is_reinstall and needs_restart) + self._plugin_manager.log_all_plugins() result = dict(result=True, url=url, needs_restart=needs_restart, needs_refresh=needs_refresh, was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, plugin=self._to_external_representation(new_plugin)) @@ -345,10 +354,13 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, def command_uninstall(self, plugin): if plugin.key == "pluginmanager": - return make_response("Can't uninstall Plugin Manager", 400) + return make_response("Can't uninstall Plugin Manager", 403) + + if not plugin.managable: + return make_response("Plugin is not managable and hence cannot be uninstalled", 403) if plugin.bundled: - return make_response("Bundled plugins cannot be uninstalled", 400) + return make_response("Bundled plugins cannot be uninstalled", 403) if plugin.origin is None: self._logger.warn(u"Trying to uninstall plugin {plugin} but origin is unknown".format(**locals())) @@ -392,7 +404,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, needs_restart = self._plugin_manager.is_restart_needing_plugin(plugin) needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin) - self._plugin_manager.mark_plugin(plugin.key, uninstalled=True) + was_pending_install = self._plugin_manager.is_plugin_marked(plugin.key, "installed") + self._plugin_manager.mark_plugin(plugin.key, + uninstalled=not was_pending_install and needs_restart, + installed=False) if not needs_restart: try: @@ -664,11 +679,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, url=plugin.url, license=plugin.license, bundled=plugin.bundled, + managable=plugin.managable, enabled=plugin.enabled, pending_enable=(not plugin.enabled and plugin.key in self._pending_enable), pending_disable=(plugin.enabled and plugin.key in self._pending_disable), - pending_install=(plugin.key in self._pending_install), - pending_uninstall=(plugin.key in self._pending_uninstall), + pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")), + pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")), origin=plugin.origin.type ) diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 75c8e2f3..8767c9ea 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -10,6 +10,7 @@ $(function() { self.config_repositoryTtl = ko.observable(); self.config_pipCommand = ko.observable(); self.config_pipAdditionalArgs = ko.observable(); + self.config_pipForceUser = ko.observable(); self.configurationDialog = $("#settings_plugin_pluginmanager_configurationdialog"); @@ -82,9 +83,22 @@ $(function() { self.pipAvailable = ko.observable(false); self.pipCommand = ko.observable(); self.pipVersion = ko.observable(); + self.pipInstallDir = ko.observable(); + self.pipUseUser = ko.observable(); self.pipUseSudo = ko.observable(); + self.pipVirtualEnv = ko.observable(); self.pipAdditionalArgs = ko.observable(); + self.pipUseSudoString = ko.computed(function() { + return self.pipUseSudo() ? "yes" : "no"; + }); + self.pipUseUserString = ko.computed(function() { + return self.pipUseUser() ? "yes" : "no"; + }); + self.pipVirtualEnvString = ko.computed(function() { + return self.pipVirtualEnv() ? "yes" : "no"; + }); + self.working = ko.observable(false); self.workingTitle = ko.observable(); self.workingDialog = undefined; @@ -101,6 +115,7 @@ $(function() { self.enableUninstall = function(data) { return self.enableManagement() && (data.origin != "entry_point" || self.pipAvailable()) + && data.managable && !data.bundled && data.key != 'pluginmanager' && !data.pending_uninstall; @@ -211,12 +226,18 @@ $(function() { if (data.available) { self.pipCommand(data.command); self.pipVersion(data.version); + self.pipInstallDir(data.install_dir); + self.pipUseUser(data.use_user); self.pipUseSudo(data.use_sudo); + self.pipVirtualEnv(data.virtual_env); self.pipAdditionalArgs(data.additional_args); } else { self.pipCommand(undefined); self.pipVersion(undefined); + self.pipInstallDir(undefined); + self.pipUseUser(undefined); self.pipUseSudo(undefined); + self.pipVirtualEnv(undefined); self.pipAdditionalArgs(undefined); } }; @@ -410,7 +431,8 @@ $(function() { repository: repository, repository_ttl: repositoryTtl, pip: pipCommand, - pip_args: pipArgs + pip_args: pipArgs, + pip_force_user: self.config_pipForceUser() } } }; @@ -426,6 +448,7 @@ $(function() { self.config_repositoryTtl(self.settingsViewModel.settings.plugins.pluginmanager.repository_ttl()); self.config_pipCommand(self.settingsViewModel.settings.plugins.pluginmanager.pip()); self.config_pipAdditionalArgs(self.settingsViewModel.settings.plugins.pluginmanager.pip_args()); + self.config_pipForceUser(self.settingsViewModel.settings.plugins.pluginmanager.pip_force_user()); }; self.installed = function(data) { diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index d399b593..ae137976 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -6,8 +6,9 @@ {% macro pluginmanager_nopip() %}
{% trans %} - The pip command could not be found. - Please configure it manually. No installation and uninstallation of plugin + The pip command could not be found or does not work correctly + for this installation of OctoPrint - please consult the log file for details + and if necessary configure it manually. No installation and uninstallation of plugin packages is possible while pip is unavailable. {% endtrans %}
{% endmacro %} @@ -42,7 +43,7 @@ -
()
+
()
 
{{ _('Homepage') }} @@ -71,9 +72,22 @@ -

- Using pip at "" (Version , additional arguments: ) -

+
+
+ + + Using pip at "", Version + + +
+
+ + Installation directory: ("--user" flag: , sudo: )
+ Virtual environment:
+ Additional Arguments: +
+
+