From 163fd83bf56520ef167fbf926fb75276d4c22227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 4 Apr 2017 15:18:37 +0200 Subject: [PATCH] PGMR: Detect necessity to reinstall So far using the "... from URL" or "... from an uploaded archive" mechanism for installing a plugin would fail without further information if the plugin to be installed was already installed. The plugin manager will now detect this situation by parsing the corresponding message from the pip output and trigger a reinstall instantly. A message about this will be logged to octoprint.log and the install output. Additionally the error handling for installation errors has been slightly improved (install output no longer says "Done!" but "Error!" with the reason as provided by the server) and the "could not install plugin from URL unknown" issue should also be solved. --- .../plugins/pluginmanager/__init__.py | 163 +++++++++++++----- .../static/css/pluginmanager.css | 2 +- .../pluginmanager/static/js/pluginmanager.js | 64 +++++-- .../static/less/pluginmanager.less | 5 + .../templates/pluginmanager_settings.jinja2 | 2 +- 5 files changed, 172 insertions(+), 64 deletions(-) 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 @@