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/README.md b/README.md index 06d5b6b0..4d5a9ba0 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,25 @@ For information about how to go about contributions of any kind, please see the Installation ------------ -Installation instructions for installing from source for different operating systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). +Installation instructions for installing from source for different operating +systems can be found [on the wiki](https://github.com/foosel/OctoPrint/wiki#assorted-guides). If you want to run OctoPrint on a Raspberry Pi you might want to take a look at [OctoPi](https://github.com/guysoft/OctoPi) which is a custom SD card image that includes OctoPrint plus dependencies. +The generic steps that should basically be done regardless of operating system +and runtime environment are the following (as *regular +user*, please keep your hands *off* of the `sudo` command here!) - this assumes +you already have Python 2.7, pip and virtualenv set up: + +1. Checkout OctoPrint: `git clone https://github.com/foosel/OctoPrint.git` +2. Change into the OctoPrint folder: `cd OctoPrint` +3. Create a user-owned virtual environment therein: `virtualenv --system-site-packages venv` +4. Install OctoPrint *into that virtual environment*: `./venv/bin/python setup.py install` + +You may then start the OctoPrint server via `/path/to/OctoPrint/venv/bin/octoprint`, see [Usage](#usage) +for details. + After installation, please make sure you follow the first-run wizard and set up access control as necessary. If you want to not only be notified about new releases but also be able to automatically upgrade to them from within 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..0dee798c 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: diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index b493ca1a..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 @@ -83,6 +84,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, repository="http://plugins.octoprint.org/plugins.json", repository_ttl=24*60, pip=None, + pip_args=None, + pip_force_user=False, dependency_links=False, hidden=[] ) @@ -93,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: @@ -187,15 +191,20 @@ 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(), pip=dict( - available=self._pip_caller.available, - command=self._pip_caller.command, - version=str(self._pip_caller.version) + 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"]) )) def on_api_command(self, command, data): @@ -328,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)) @@ -342,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())) @@ -389,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: @@ -453,6 +471,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, if "--process-dependency-links" in args: self._log_message(u"Installation needs to process external dependencies, that might make it take a bit longer than usual depending on the pip version") + additional_args = self._settings.get(["pip_args"]) + if additional_args: + args.append(additional_args) + return self._pip_caller.execute(*args) def _log_message(self, *lines): @@ -657,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 254b29d5..1eed2a2f 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -76,6 +76,8 @@ $(function() { self.config_repositoryUrl = ko.observable(); 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"); @@ -148,6 +150,21 @@ $(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(); @@ -165,6 +182,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; @@ -275,9 +293,19 @@ $(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); } }; @@ -463,12 +491,19 @@ $(function() { repositoryTtl = null; } + var pipArgs = self.config_pipAdditionalArgs(); + if (pipArgs != undefined && pipArgs.trim() == "") { + pipArgs = null; + } + var data = { plugins: { pluginmanager: { repository: repository, repository_ttl: repositoryTtl, - pip: pipCommand + pip: pipCommand, + pip_args: pipArgs, + pip_force_user: self.config_pipForceUser() } } }; @@ -483,6 +518,8 @@ $(function() { self.config_repositoryUrl(self.settingsViewModel.settings.plugins.pluginmanager.repository()); 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 7c00534a..72cbc7ca 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -6,14 +6,25 @@ {% macro pluginmanager_nopip() %}
{% trans %} - The pip command could not be detected automatically, - please configure it manually. No installation and uninstallation of plugin + The pip command could not be found. + Please configure it manually. No installation and uninstallation of plugin packages is possible while pip is unavailable. {% endtrans %}
{% endmacro %} +{% macro pluginmanager_sudopip() %} +
{% trans %} + The pip command is configured to use sudo. This + is not recommended due to security reasons. It is strongly + suggested you install OctoPrint under a + user-owned virtual environment + so that the use of sudo is not needed for plugin management. +{% endtrans %}
+{% endmacro %} + {{ pluginmanager_printing() }} {{ pluginmanager_nopip() }} +{{ pluginmanager_sudopip() }}
@@ -31,7 +42,7 @@ -
()
+
()
 
{{ _('Homepage') }} @@ -60,9 +71,22 @@ -

- Using pip at "" (Version ) -

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