diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index 65b2d480..52ae2684 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -261,7 +261,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, plugin_name = data["plugin"] if "plugin" in data else None return self.command_install(url=url, force="force" in data and data["force"] in valid_boolean_trues, - dependency_links="dependency_links" in data and data["dependency_links"] in valid_boolean_trues, + dependency_links="dependency_links" in data + and data["dependency_links"] in valid_boolean_trues, reinstall=plugin_name) elif command == "uninstall": @@ -282,37 +283,84 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, def command_install(self, url=None, path=None, force=False, reinstall=None, dependency_links=False): if url is not None: - pip_args = ["install", sarge.shell_quote(url)] + source = url + source_type = "url" + already_installed_check = lambda line: url in line + elif path is not None: - pip_args = ["install", sarge.shell_quote(path)] + path = os.path.abspath(path) + path_url = "file://" + path + if os.sep != "/": + # windows gets special handling + path = path.replace(os.sep, "/").lower() + path_url = "file:///" + path + + source = path + source_type = "path" + already_installed_check = lambda line: path_url in line.lower() # lower case in case of windows + else: raise ValueError("Either URL or path must be provided") + self._logger.info("Installing plugin from {}".format(source)) + pip_args = ["install", sarge.shell_quote(source)] + if dependency_links or self._settings.get_boolean(["dependency_links"]): pip_args.append("--process-dependency-links") - all_plugins_before = self._plugin_manager.find_plugins() + all_plugins_before = self._plugin_manager.find_plugins(existing=dict()) + already_installed_string = "Requirement already satisfied (use --upgrade to upgrade)" success_string = "Successfully installed" failure_string = "Could not install" + try: returncode, stdout, stderr = self._call_pip(pip_args) + + # pip's output for a package that is already installed looks something like any of these: + # + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # https://example.com/foobar.zip in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # file:///tmp/foobar.zip in + # Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \ + # file:///C:/Temp/foobar.zip in + # + # If we detect any of these matching what we just tried to install, we'll need to trigger a second + # install with reinstall flags. + + if not force and any(map(lambda x: x.strip().startswith(already_installed_string) and already_installed_check(x), + stdout)): + self._logger.info("Plugin to be installed from {} was already installed, forcing a reinstall".format(source)) + self._log_message("Looks like the plugin was already installed. Forcing a reinstall.") + force = True except: self._logger.exception("Could not install plugin from %s" % url) return make_response("Could not install plugin from URL, see the log for more details", 500) else: if force: + # We don't use --upgrade here because that will also happily update all our dependencies - we'd rather + # do that in a controlled manner pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] try: returncode, stdout, stderr = self._call_pip(pip_args) except: - self._logger.exception("Could not install plugin from %s" % url) - return make_response("Could not install plugin from URL, see the log for more details", 500) + self._logger.exception("Could not install plugin from {}".format(source)) + return make_response("Could not install plugin from source {}, see the log for more details" + .format(source), 500) try: - result_line = filter(lambda x: x.startswith(success_string) or x.startswith(failure_string), stdout)[-1] + result_line = filter(lambda x: x.startswith(success_string) or x.startswith(failure_string), + stdout)[-1] except IndexError: - result = dict(result=False, reason="Could not parse output from pip") + self._logger.error("Installing the plugin from {} failed, could not parse output from pip. " + "See plugin_pluginmanager_console.log for generated output".format(source)) + result = dict(result=False, + source=source, + source_type=source_type, + reason="Could not parse output from pip, see plugin_pluginmanager_console.log " + "for generated output") self._send_result_notification("install", result) return jsonify(result) @@ -325,8 +373,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, # Successfully installed OctoPrint-Plugin Dependency-One Dependency-Two # Cleaning up... # - # So we'll need to fetch the "Successfully installed" line, strip the "Successfully" part, then split by whitespace - # and strip to get all installed packages. + # So we'll need to fetch the "Successfully installed" line, strip the "Successfully" part, then split + # by whitespace and strip to get all installed packages. # # We then need to iterate over all known plugins and see if either the package name or the package name plus # version number matches one of our installed packages. If it does, that's our installed plugin. @@ -338,62 +386,53 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, result_line = result_line.strip() if not result_line.startswith(success_string): - result = dict(result=False, reason="Pip did not report successful installation") + self._logger.error("Installing the plugin from {} failed, pip did not report successful installation" + .format(source)) + result = dict(result=False, + source=source, + source_type=source_type, + reason="Pip did not report successful installation") self._send_result_notification("install", result) return jsonify(result) installed = map(lambda x: x.strip(), result_line[len(success_string):].split(" ")) all_plugins_after = self._plugin_manager.find_plugins(existing=dict(), ignore_uninstalled=False) - for key, plugin in all_plugins_after.items(): - if plugin.origin is None or plugin.origin.type != "entry_point": - continue - - package_name = plugin.origin.package_name - package_version = plugin.origin.package_version - versioned_package = "{package_name}-{package_version}".format(**locals()) - - if package_name in installed or versioned_package in installed: - # exact match, we are done here - new_plugin_key = key - new_plugin = plugin - break - - else: - # it might still be a version that got stripped by python's package resources, e.g. 1.4.5a0 => 1.4.5a - found = False - - for inst in installed: - if inst.startswith(versioned_package): - found = True - break - - if found: - new_plugin_key = key - new_plugin = plugin - break - else: - self._logger.warn("The plugin was installed successfully, but couldn't be found afterwards to initialize properly during runtime. Please restart OctoPrint.") - result = dict(result=True, url=url, needs_restart=True, needs_refresh=True, was_reinstalled=False, plugin="unknown") + new_plugin = self._find_installed_plugin(installed, plugins=all_plugins_after) + if new_plugin is None: + self._logger.warn("The plugin was installed successfully, but couldn't be found afterwards to " + "initialize properly during runtime. Please restart OctoPrint.") + result = dict(result=True, + source=source, + source_type=source_type, + needs_restart=True, + needs_refresh=True, + was_reinstalled=False, + plugin="unknown") self._send_result_notification("install", result) return jsonify(result) 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) + 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, + 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() + self._logger.info("The plugin was installed successfully: {}, version {}".format(new_plugin.name, new_plugin.version)) result = dict(result=True, - url=url, + source=source, + source_type=source_type, needs_restart=needs_restart, needs_refresh=needs_refresh, - was_reinstalled=new_plugin_key in all_plugins_before or reinstall is not None, + was_reinstalled=new_plugin.key in all_plugins_before or reinstall is not None, plugin=self._to_external_plugin(new_plugin)) self._send_result_notification("install", result) return jsonify(result) @@ -512,6 +551,36 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._send_result_notification(command, result) return jsonify(result) + def _find_installed_plugin(self, packages, plugins=None): + if plugins is None: + plugins = self._plugin_manager.find_plugins(existing=dict(), ignore_uninstalled=False) + + for key, plugin in plugins.items(): + if plugin.origin is None or plugin.origin.type != "entry_point": + continue + + package_name = plugin.origin.package_name + package_version = plugin.origin.package_version + versioned_package = "{package_name}-{package_version}".format(**locals()) + + if package_name in packages or versioned_package in packages: + # exact match, we are done here + return plugin + + else: + # it might still be a version that got stripped by python's package resources, e.g. 1.4.5a0 => 1.4.5a + found = False + + for inst in packages: + if inst.startswith(versioned_package): + found = True + break + + if found: + return plugin + + return None + def _send_result_notification(self, action, result): notification = dict(type="result", action=action) notification.update(result) diff --git a/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css b/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css index b1cbc9af..91383181 100644 --- a/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css +++ b/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css @@ -1 +1 @@ -table th.settings_plugin_plugin_manager_plugins_name,table td.settings_plugin_plugin_manager_plugins_name{text-overflow:ellipsis;text-align:left}table th.settings_plugin_plugin_manager_plugins_actions,table td.settings_plugin_plugin_manager_plugins_actions{text-align:center;width:80px}table th.settings_plugin_plugin_manager_plugins_actions a,table td.settings_plugin_plugin_manager_plugins_actions a{text-decoration:none;color:#000}table th.settings_plugin_plugin_manager_plugins_actions a.disabled,table td.settings_plugin_plugin_manager_plugins_actions a.disabled{color:#ccc;cursor:default}#settings_plugin_pluginmanager_repositorydialog .slimScrollDiv{margin-bottom:20px}#settings_plugin_pluginmanager_repositorydialog h4{position:relative}#settings_plugin_pluginmanager_repositorydialog h4 a.dropdown-toggle{color:inherit;text-decoration:none;font-size:14px}#settings_plugin_pluginmanager_repositorydialog h4 ul.dropdown-menu{font-size:14px}#settings_plugin_pluginmanager_repositorydialog .form-search{text-align:center;margin-bottom:5px!important}#settings_plugin_pluginmanager_repositorydialog .form-inline{padding:5px;padding-right:10px;margin-bottom:0}#settings_plugin_pluginmanager_repositorydialog .form-inline .help-block{margin-bottom:0;font-size:85%}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable{overflow:hidden;width:100%;height:300px;background-image:url("../img/repo_unavailable.png");text-align:center;display:table}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable div{display:table-cell;vertical-align:middle}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list{overflow:hidden;width:auto;height:300px}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list .entry{border-bottom:1px solid #ddd;padding:5px;padding-right:10px}#settings_plugin_pluginmanager_workingdialog_output .message{font-weight:bold}#settings_plugin_pluginmanager_workingdialog_output .stdout{color:#333}#settings_plugin_pluginmanager_workingdialog_output .stderr{color:#900}#settings_plugin_pluginmanager_workingdialog_output .call{color:#009} \ No newline at end of file +table td.settings_plugin_plugin_manager_plugins_name,table th.settings_plugin_plugin_manager_plugins_name{text-overflow:ellipsis;text-align:left}table td.settings_plugin_plugin_manager_plugins_actions,table th.settings_plugin_plugin_manager_plugins_actions{text-align:center;width:80px}table td.settings_plugin_plugin_manager_plugins_actions a,table th.settings_plugin_plugin_manager_plugins_actions a{text-decoration:none;color:#000}table td.settings_plugin_plugin_manager_plugins_actions a.disabled,table th.settings_plugin_plugin_manager_plugins_actions a.disabled{color:#ccc;cursor:default}#settings_plugin_pluginmanager_repositorydialog .slimScrollDiv{margin-bottom:20px}#settings_plugin_pluginmanager_repositorydialog h4{position:relative}#settings_plugin_pluginmanager_repositorydialog h4 a.dropdown-toggle{color:inherit;text-decoration:none;font-size:14px}#settings_plugin_pluginmanager_repositorydialog h4 ul.dropdown-menu{font-size:14px}#settings_plugin_pluginmanager_repositorydialog .form-search{text-align:center;margin-bottom:5px!important}#settings_plugin_pluginmanager_repositorydialog .form-inline{padding:5px 10px 5px 5px;margin-bottom:0}#settings_plugin_pluginmanager_repositorydialog .form-inline .help-block{margin-bottom:0;font-size:85%}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable{overflow:hidden;width:100%;height:300px;background-image:url(../img/repo_unavailable.png);text-align:center;display:table}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_unavailable div{display:table-cell;vertical-align:middle}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list{overflow:hidden;width:auto;height:300px}#settings_plugin_pluginmanager_repositorydialog #settings_plugin_pluginmanager_repositorydialog_list .entry{border-bottom:1px solid #ddd;padding:5px 10px 5px 5px}#settings_plugin_pluginmanager_workingdialog_output .message{font-weight:700}#settings_plugin_pluginmanager_workingdialog_output .error{font-weight:700;color:#900}#settings_plugin_pluginmanager_workingdialog_output .stdout{color:#333}#settings_plugin_pluginmanager_workingdialog_output .stderr{color:#900}#settings_plugin_pluginmanager_workingdialog_output .call{color:#009} \ No newline at end of file diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 7c3b6ac0..57b1b92d 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -279,7 +279,13 @@ $(function() { }); }, done: function(e, data) { - self._markDone(); + var response = data.result; + if (response.result) { + self._markDone(); + } else { + self._markDone(response.reason); + } + self.uploadButton.unbind("click"); self.uploadFilename(undefined); }, @@ -290,7 +296,7 @@ $(function() { type: "error", hide: false }); - self._markDone(); + self._markDone("Could not install plugin, unknown error."); self.uploadButton.unbind("click"); self.uploadFilename(undefined); } @@ -498,20 +504,23 @@ $(function() { } self._markWorking(workTitle, workText); - var onSuccess = function() { + var onSuccess = function(response) { + if (response.result) { + self._markDone(); + } else { + self._markDone(response.reason) + } self.requestData(); self.installUrl(""); }, onError = function() { + self._markDone("Could not install plugin, unknown error, please consult octoprint.log for details"); new PNotify({ title: gettext("Something went wrong"), text: gettext("Please consult octoprint.log for details"), type: "error", hide: false }); - }, - onAlways = function() { - self._markDone(); }; if (reinstall) { @@ -767,9 +776,14 @@ $(function() { self.workingDialog.modal({keyboard: false, backdrop: "static", show: true}); }; - self._markDone = function() { + self._markDone = function(error) { self.working(false); - self.loglines.push({line: gettext("Done!"), stream: "message"}); + if (error) { + self.loglines.push({line: gettext("Error!"), stream: "error"}); + self.loglines.push({line: error, stream: "error"}) + } else { + self.loglines.push({line: gettext("Done!"), stream: "message"}); + } self._scrollWorkingOutputToEnd(); }; @@ -1071,22 +1085,42 @@ $(function() { } titleError = gettext("Something went wrong"); - var url = "unknown"; - if (data.hasOwnProperty("url")) { - url = data.url; + var source = "unknown"; + if (data.hasOwnProperty("source")) { + source = data.source; + } + var sourceType = "unknown"; + if (data.hasOwnProperty("source_type")) { + sourceType = data.source_type; } if (data.hasOwnProperty("reason")) { if (data.was_reinstalled) { - textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url}); + if (sourceType == "path") { + textError = _.sprintf(gettext("Reinstalling the plugin from file failed: %(reason)s"), {reason: data.reason}); + } else { + textError = _.sprintf(gettext("Reinstalling the plugin from \"%(source)s\" failed: %(reason)s"), {reason: data.reason, source: source}); + } } else { - textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed: %(reason)s"), {reason: data.reason, url: url}); + if (sourceType == "path") { + textError = _.sprintf(gettext("Installing the plugin from file failed: %(reason)s"), {reason: data.reason}); + } else { + textError = _.sprintf(gettext("Installing the plugin from \"%(source)s\" failed: %(reason)s"), {reason: data.reason, source: source}); + } } } else { if (data.was_reinstalled) { - textError = _.sprintf(gettext("Reinstalling the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url}); + if (sourceType == "path") { + textError = gettext("Reinstalling the plugin from file failed, please see the log for details."); + } else { + textError = _.sprintf(gettext("Reinstalling the plugin from \"%(source)s\" failed, please see the log for details."), {source: source}); + } } else { - textError = _.sprintf(gettext("Installing the plugin from URL \"%(url)s\" failed, please see the log for details."), {url: url}); + if (sourceType == "path") { + textError = gettext("Installing the plugin from file failed, please see the log for details."); + } else { + textError = _.sprintf(gettext("Installing the plugin from \"%(source)s\" failed, please see the log for details."), {source: source}); + } } } diff --git a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less index a14c2f6b..2b591e9c 100644 --- a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less +++ b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less @@ -89,6 +89,11 @@ table { font-weight: bold; } + .error { + font-weight: bold; + color: #990000; + } + .stdout { color: #333333; } diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 3f0b7124..93b65dfd 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -108,7 +108,7 @@