From a580482c294d0431e51f360ccb3d5e5988339269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 22 Jun 2015 14:28:05 +0200 Subject: [PATCH 01/13] Allow asynchronous system commands & commands w/ ignored result That should get rid of error messages for things like shutdown commands. --- src/octoprint/server/api/__init__.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index d50bb922..a979053a 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -152,6 +152,8 @@ def performSystemAction(): available_actions = s().get(["system", "actions"]) for availableAction in available_actions: if availableAction["action"] == action: + async = availableAction["async"] if "async" in availableAction else False + ignore = availableAction["ignore"] if "ignore" in availableAction else False logger.info("Performing command: %s" % availableAction["command"]) try: # Note: we put the command in brackets since sarge (up to the most recently released version) has @@ -159,15 +161,18 @@ def performSystemAction(): # workaround again # # See https://bitbucket.org/vinay.sajip/sarge/issue/21/behavior-is-not-like-popen-using-shell - p = sarge.run([availableAction["command"]], stderr=sarge.Capture(), shell=True) - if p.returncode != 0: - returncode = p.returncode - stderr_text = p.stderr.text - logger.warn("Command failed with return code %i: %s" % (returncode, stderr_text)) - return make_response(("Command failed with return code %i: %s" % (returncode, stderr_text), 500, [])) + p = sarge.run([availableAction["command"]], stderr=sarge.Capture(), shell=True, async=async) + if not async: + if not ignore and p.returncode != 0: + returncode = p.returncode + stderr_text = p.stderr.text + logger.warn("Command failed with return code %i: %s" % (returncode, stderr_text)) + return make_response(("Command failed with return code %i: %s" % (returncode, stderr_text), 500, [])) except Exception, e: - logger.warn("Command failed: %s" % e) - return make_response(("Command failed: %s" % e, 500, [])) + if not ignore: + logger.warn("Command failed: %s" % e) + return make_response(("Command failed: %s" % e, 500, [])) + break return NO_CONTENT From 1a7193e113b219619cdce7ae8d22810e9f4d24b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Mon, 22 Jun 2015 16:55:32 +0200 Subject: [PATCH 02/13] System commands with ignore set to true also ignore http errors --- src/octoprint/static/js/app/viewmodels/navigation.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/octoprint/static/js/app/viewmodels/navigation.js b/src/octoprint/static/js/app/viewmodels/navigation.js index ee8a4065..c9f3b9b1 100644 --- a/src/octoprint/static/js/app/viewmodels/navigation.js +++ b/src/octoprint/static/js/app/viewmodels/navigation.js @@ -28,15 +28,17 @@ $(function() { new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"}); }, error: function(jqXHR, textStatus, errorThrown) { - var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "

"; - error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); - new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); + if (!action.hasOwnProperty("ignore") || !action.ignore) { + var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "

"; + error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); + new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); + } } }) }; if (action.confirm) { showConfirmationDialog(action.confirm, function (e) { - callback(); + callback(); }); } else { callback(); @@ -49,4 +51,4 @@ $(function() { ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"], "#navbar" ]); -}); \ No newline at end of file +}); From b32f9bddd23dd0203de12faf92ff5721487fc02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 23 Jun 2015 11:35:23 +0200 Subject: [PATCH 03/13] New util class for performing pip calls Will lookup the correct pip command to use if not provided as constructor parameter and determine the version. Implements __lt__, __le__, __gt__ and __ge__ for "less/greater than" checks against the pip version, for dynamicalle changing call parameters provided to the execute method based on availability. Will allow adjusting software update and plugin manager plugins to only use --process-dependency-links for pip versions >= 1.5 --- src/octoprint/util/__init__.py | 2 +- src/octoprint/util/pip.py | 180 +++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/octoprint/util/pip.py diff --git a/src/octoprint/util/__init__.py b/src/octoprint/util/__init__.py index a682578a..9b63618e 100644 --- a/src/octoprint/util/__init__.py +++ b/src/octoprint/util/__init__.py @@ -681,4 +681,4 @@ class InvariantContainer(object): return len(self._data) def __iter__(self): - return self._data.__iter__() \ No newline at end of file + return self._data.__iter__() diff --git a/src/octoprint/util/pip.py b/src/octoprint/util/pip.py new file mode 100644 index 00000000..f670edf7 --- /dev/null +++ b/src/octoprint/util/pip.py @@ -0,0 +1,180 @@ +# coding=utf-8 +from __future__ import absolute_import + +__author__ = "Gina Häußge " +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" + + +import sarge +import sys +import logging + + +class UnknownPip(Exception): + pass + +class PipCaller(object): + def __init__(self, configured=None): + self._logger = logging.getLogger(__name__) + + self._configured = configured + + self._command = None + self._version = None + + self._command, self._version = self._find_pip() + + self.refresh = False + self.on_log_call = lambda *args, **kwargs: None + self.on_log_stdout = lambda *args, **kwargs: None + self.on_log_stderr = lambda *args, **kwargs: None + + def __le__(self, other): + return self.version is not None and self.version <= other + + def __lt__(self, other): + return self.version is not None and self.version < other + + def __ge__(self, other): + return self.version is not None and self.version >= other + + def __gt__(self, other): + return self.version is not None and self.version > other + + @property + def command(self): + return self._command + + @property + def version(self): + return self._version + + @property + def available(self): + return self._command is not None + + def execute(self, *args): + if self.refresh: + self._command, self._version = self._find_pip() + self.refresh = False + + if self._command is None: + raise UnknownPip() + + command = [self._command] + list(args) + + joined_command = " ".join(command) + + self._logger.debug(u"Calling: {}".format(joined_command)) + self.on_log_call(joined_command) + + p = sarge.run(joined_command, shell=True, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + p.wait_events() + + all_stdout = [] + all_stderr = [] + try: + while p.returncode is None: + line = p.stderr.readline(timeout=0.5) + if line: + self._log_stderr(line) + all_stderr.append(line) + + line = p.stdout.readline(timeout=0.5) + if line: + self._log_stdout(line) + all_stdout.append(line) + + p.commands[0].poll() + + finally: + p.close() + + stderr = p.stderr.text + if stderr: + split_lines = stderr.split("\n") + self._log_stderr(*split_lines) + all_stderr += split_lines + + stdout = p.stdout.text + if stdout: + split_lines = stdout.split("\n") + self._log_stdout(*split_lines) + all_stdout += split_lines + + return p.returncode, all_stdout, all_stderr + + + def _find_pip(self): + pip_command = self._configured + pip_version = None + + if pip_command is None: + import os + python_command = sys.executable + binary_dir = os.path.dirname(python_command) + + pip_command = os.path.join(binary_dir, "pip") + if sys.platform == "win32": + # Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly + # for a non-virtualenv install (e.g. global install) the pip binary will not be located in the + # same folder as python.exe, but in a subfolder Scripts, e.g. + # + # C:\Python2.7\ + # |- python.exe + # `- Scripts + # `- pip.exe + + # virtual env? + pip_command = os.path.join(binary_dir, "pip.exe") + + if not os.path.isfile(pip_command): + # nope, let's try the Scripts folder then + scripts_dir = os.path.join(binary_dir, "Scripts") + if os.path.isdir(scripts_dir): + pip_command = os.path.join(scripts_dir, "pip.exe") + + if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK): + pip_command = None + + if pip_command is not None: + self._logger.debug("Found pip at {}, going to figure out its version".format(pip_command)) + command = [pip_command, "--version"] + p = sarge.run(" ".join(command), shell=True, stdout=sarge.Capture(), stderr=sarge.Capture()) + + if p.returncode != 0: + self._logger.warn("Error while trying to run pip --version: {}".format(p.stderr.text)) + pip_command = None + + output = p.stdout.text + # output should look something like this: + # + # pip from () + # + # we'll just split on whitespace and then try to use the second entry + + if not output.startswith("pip"): + self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) + + split_output = map(lambda x: x.strip(), output.split()) + if len(split_output) < 2: + self._logger.warn("pip command returned unparseable output, can't determine version: {}".format(output)) + + version_segment = split_output[1] + + from pkg_resources import parse_version + try: + pip_version = parse_version(version_segment) + except: + self._logger.exception("Error while trying to parse version string from pip command") + else: + self._logger.info("Found pip at {}, version is {}".format(pip_command, version_segment)) + + return pip_command, pip_version + + def _log_stdout(self, *lines): + self.on_log_stdout(*lines) + + def _log_stderr(self, *lines): + self.on_log_stderr(*lines) From 71ccc47717746a6ba28869fe8322a0b1d0d36003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 23 Jun 2015 11:36:27 +0200 Subject: [PATCH 04/13] Plugin Manager: Use new PipCaller class and only provide dependency-links parameter if supported by pip version --- .../plugins/pluginmanager/__init__.py | 83 ++++--------------- 1 file changed, 18 insertions(+), 65 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index e59de75e..cee575b5 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -12,6 +12,7 @@ import octoprint.plugin.core from octoprint.settings import valid_boolean_trues from octoprint.server.util.flask import restricted_access from octoprint.server import admin_permission +from octoprint.util.pip import PipCaller, UnknownPip from flask import jsonify, make_response from flask.ext.babel import gettext @@ -22,6 +23,7 @@ import sys import requests import re import os +import pkg_resources class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, octoprint.plugin.TemplatePlugin, @@ -36,6 +38,9 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._pending_install = set() self._pending_uninstall = set() + self._pip_caller = None + self._pip_version_dependency_links = pkg_resources.parse_version("1.5") + self._repository_available = False self._repository_plugins = [] self._repository_cache_path = None @@ -46,6 +51,11 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._repository_cache_path = os.path.join(self._settings.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.on_log_call = self._log_call + self._pip_caller.on_log_stdout = self._log_stdout + self._pip_caller.on_log_stderr = self._log_stderr + ##~~ StartupPlugin def on_startup(self, host, port): @@ -72,6 +82,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, def on_settings_save(self, data): octoprint.plugin.SettingsPlugin.on_settings_save(self, data) self._repository_cache_ttl = self._settings.get_int(["repository_ttl"]) * 60 + self._pip_caller.refresh = True ##~~ AssetPlugin @@ -396,74 +407,16 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._plugin_manager.send_plugin_message(self._identifier, notification) def _call_pip(self, args): - pip_command = self._settings.get(["pip"]) - if pip_command is None: - import os - python_command = sys.executable - binary_dir = os.path.dirname(python_command) + if self._pip_caller is None or not self._pip_caller.available: + raise RuntimeError(u"No pip available, can't operate".format(**locals())) - pip_command = os.path.join(binary_dir, "pip") - if sys.platform == "win32": - # Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly - # for a non-virtualenv install (e.g. global install) the pip binary will not be located in the - # same folder as python.exe, but in a subfolder Scripts, e.g. - # - # C:\Python2.7\ - # |- python.exe - # `- Scripts - # `- pip.exe + if "--process-dependency-links" in args and self._pip_caller < self._pip_version_dependency_links: + args.remove("--process-dependency-links") - # virtual env? - pip_command = os.path.join(binary_dir, "pip.exe") + return self._pip_caller.execute(*args) - if not os.path.isfile(pip_command): - # nope, let's try the Scripts folder then - scripts_dir = os.path.join(binary_dir, "Scripts") - if os.path.isdir(scripts_dir): - pip_command = os.path.join(scripts_dir, "pip.exe") - - if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK): - raise RuntimeError(u"No pip path configured and {pip_command} does not exist or is not executable, can't install".format(**locals())) - - command = [pip_command] + args - - self._logger.debug(u"Calling: {}".format(" ".join(command))) - - p = sarge.run(" ".join(command), shell=True, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) - p.wait_events() - - all_stdout = [] - all_stderr = [] - try: - while p.returncode is None: - line = p.stderr.readline(timeout=0.5) - if line: - self._log_stderr(line) - all_stderr.append(line) - - line = p.stdout.readline(timeout=0.5) - if line: - self._log_stdout(line) - all_stdout.append(line) - - p.commands[0].poll() - - finally: - p.close() - - stderr = p.stderr.text - if stderr: - split_lines = stderr.split("\n") - self._log_stderr(*split_lines) - all_stderr += split_lines - - stdout = p.stdout.text - if stdout: - split_lines = stdout.split("\n") - self._log_stdout(*split_lines) - all_stdout += split_lines - - return p.returncode, all_stdout, all_stderr + def _log_call(self, *lines): + self._log(lines, prefix=" ", stream="call") def _log_stdout(self, *lines): self._log(lines, prefix=">", stream="stdout") From ac151d90199c0a7f052ee91289afc46441a1276a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 23 Jun 2015 11:36:41 +0200 Subject: [PATCH 05/13] Software Update: Use new PipCaller class and only provide dependency-links parameter if supported by pip version --- .../plugins/softwareupdate/updaters/pip.py | 117 ++++++------------ 1 file changed, 38 insertions(+), 79 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/updaters/pip.py b/src/octoprint/plugins/softwareupdate/updaters/pip.py index c6f8e590..545b3dca 100644 --- a/src/octoprint/plugins/softwareupdate/updaters/pip.py +++ b/src/octoprint/plugins/softwareupdate/updaters/pip.py @@ -7,114 +7,73 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import logging -import sarge -import sys +import pkg_resources + +from octoprint.util.pip import PipCaller, UnknownPip logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip") console_logger = logging.getLogger("octoprint.plugins.softwareupdate.updaters.pip.console") +_pip_callers = dict() +_pip_version_dependency_links = pkg_resources.parse_version("1.5") + def can_perform_update(target, check): - return "pip" in check + pip_caller = _get_pip_caller(command=check["pip_command"] if "pip_command" in check else None) + return "pip" in check and pip_caller is not None and pip_caller.available + +def _get_pip_caller(command=None): + key = command + if command is None: + key = "__default" + + if not key in _pip_callers: + try: + _pip_callers[key] = PipCaller(configured=command) + _pip_callers[key].on_log_call = _log_call + _pip_callers[key].on_log_stdout = _log_stdout + _pip_callers[key].on_log_stderr = _log_stderr + except UnknownPip: + _pip_callers[key] = None + + return _pip_callers[key] def perform_update(target, check, target_version): pip_command = None if "pip_command" in check: pip_command = check["pip_command"] + pip_caller = _get_pip_caller(command=pip_command) + if pip_caller is None: + raise RuntimeError("Can't run pip") + install_arg = check["pip"].format(target_version=target_version) logger.debug("Target: %s, executing pip install %s" % (target, install_arg)) pip_args = ["install", check["pip"].format(target_version=target_version, target=target_version)] - if "dependency_links" in check and check["dependency_links"]: + if "dependency_links" in check and check["dependency_links"] and pip_caller >= _pip_version_dependency_links: pip_args += ["--process-dependency-links"] - _call_pip(pip_args, pip_command=pip_command) + pip_caller.execute(*pip_args) logger.debug("Target. %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg)) pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"] - _call_pip(pip_args, pip_command=pip_command) + + pip_caller.execute(*pip_args) return "ok" -def _call_pip(args, pip_command=None): - if pip_command is None: - import os - python_command = sys.executable - binary_dir = os.path.dirname(python_command) - - pip_command = os.path.join(binary_dir, "pip") - if sys.platform == "win32": - # Windows is a bit special... first of all the file will be called pip.exe, not just pip, and secondly - # for a non-virtualenv install (e.g. global install) the pip binary will not be located in the - # same folder as python.exe, but in a subfolder Scripts, e.g. - # - # C:\Python2.7\ - # |- python.exe - # `- Scripts - # `- pip.exe - - # virtual env? - pip_command = os.path.join(binary_dir, "pip.exe") - - if not os.path.isfile(pip_command): - # nope, let's try the Scripts folder then - scripts_dir = os.path.join(binary_dir, "Scripts") - if os.path.isdir(scripts_dir): - pip_command = os.path.join(scripts_dir, "pip.exe") - - if not os.path.isfile(pip_command) or not os.access(pip_command, os.X_OK): - raise RuntimeError(u"No pip path configured and {pip_command} does not exist or is not executable, can't install".format(**locals())) - - command = [pip_command] + args - - logger.debug(u"Calling: {}".format(" ".join(command))) - - p = sarge.run(" ".join(command), shell=True, async=True, stdout=sarge.Capture(), stderr=sarge.Capture()) - p.wait_events() - - all_stdout = [] - all_stderr = [] - try: - while p.returncode is None: - line = p.stderr.readline(timeout=0.5) - if line: - _log_stderr(line) - all_stderr.append(line) - - line = p.stdout.readline(timeout=0.5) - if line: - _log_stdout(line) - all_stdout.append(line) - - p.commands[0].poll() - - finally: - p.close() - - stderr = p.stderr.text - if stderr: - split_lines = stderr.split("\n") - _log_stderr(*split_lines) - all_stderr += split_lines - - stdout = p.stdout.text - if stdout: - split_lines = stdout.split("\n") - _log_stdout(*split_lines) - all_stdout += split_lines - - return p.returncode, all_stdout, all_stderr +def _log_call(*lines): + _log(lines, prefix=" ") def _log_stdout(*lines): - _log(lines, prefix=">", stream="stdout") + _log(lines, prefix=">") def _log_stderr(*lines): - _log(lines, prefix="!", stream="stderr") + _log(lines, prefix="!") -def _log(lines, prefix=None, stream=None, strip=True): - if strip: - lines = map(lambda x: x.strip(), lines) +def _log(lines, prefix=None): + lines = map(lambda x: x.strip(), lines) for line in lines: console_logger.debug(u"{prefix} {line}".format(**locals())) From 5decb23f236f08d9e9ae2f98558984fc1234eb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 23 Jun 2015 11:37:07 +0200 Subject: [PATCH 06/13] Plugin Manager: color coding call/stdout/stderr in install/uninstall logs --- .../pluginmanager/static/css/pluginmanager.css | 2 +- .../static/less/pluginmanager.less | 17 +++++++++++++++++ .../templates/pluginmanager_settings.jinja2 | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css b/src/octoprint/plugins/pluginmanager/static/css/pluginmanager.css index 76eea944..b1cbc9af 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} \ No newline at end of file +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 diff --git a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less index 6f21d187..a14c2f6b 100644 --- a/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less +++ b/src/octoprint/plugins/pluginmanager/static/less/pluginmanager.less @@ -84,3 +84,20 @@ table { } } +#settings_plugin_pluginmanager_workingdialog_output { + .message { + font-weight: bold; + } + + .stdout { + color: #333333; + } + + .stderr { + color: #990000; + } + + .call { + color: #000099; + } +} diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 8cffb17f..cd92a6a3 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -53,7 +53,7 @@

{{ _('This does not look like a valid plugin archive. Valid plugin archives should be either zip files or tarballs and have the extension ".zip", ".tar.gz", ".tgz" or ".tar"') }} - -
- -
-
-
-
- -
-
-
-
-