diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 0dee798c..a5c223f3 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -933,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: @@ -1027,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/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 1eed2a2f..3782fdd4 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -72,6 +72,7 @@ $(function() { self.loginState = parameters[0]; self.settingsViewModel = parameters[1]; self.printerState = parameters[2]; + self.systemViewModel = parameters[3]; self.config_repositoryUrl = ko.observable(); self.config_repositoryTtl = ko.observable(); @@ -171,6 +172,20 @@ $(function() { self.workingDialog = undefined; self.workingOutput = undefined; + self.restartCommandSpec = undefined; + self.systemViewModel.systemActions.subscribe(function() { + var lastResponse = self.systemViewModel.lastCommandResponse; + if (!lastResponse || !lastResponse.core) { + self.restartCommandSpec = undefined; + return; + } + + var restartSpec = _.filter(lastResponse.core, function(spec) { return spec.action == "restart" }); + self.restartCommandSpec = restartSpec != undefined && restartSpec.length > 0 ? restartSpec[0] : undefined; + }); + + self.notifications = []; + self.enableManagement = ko.computed(function() { return !self.printerState.isPrinting(); }); @@ -535,15 +550,65 @@ $(function() { }; self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, titleError, textError) { + var notification; + + var beforeClose = function(notification) { + self.notifications = _.without(self.notifications, notification); + }; + if (response.result) { if (response.needs_restart) { - new PNotify({ + var options = { title: titleSuccess, text: textRestart, + buttons: { + closer: false, + sticker: false + }, + callbacks: { + before_close: beforeClose + }, hide: false - }); + }; + + if (self.restartCommandSpec) { + options.confirm = { + confirm: true, + buttons: [{ + text: gettext("Restart now"), + click: function () { + showConfirmationDialog({ + message: gettext("This will restart your OctoPrint server."), + onproceed: function() { + $.ajax({ + url: self.restartCommandSpec.resource, + type: "POST", + dataType: "json", + data: "{}", + contentType: "application/json; charset=UTF-8", + success: function() { + new PNotify({ + title: gettext("Restart in progress"), + text: gettext("The server is now being restarted in the background") + }) + }, + error: function() { + new PNotify({ + title: gettext("Something went wrong"), + text: gettext("Trying to restart the server produced an error, please check octoprint.log for details. You'll have to restart manually.") + }) + } + }); + } + }); + } + }] + } + } + + notification = PNotify.singleButtonNotify(options); } else if (response.needs_refresh) { - new PNotify({ + notification = PNotify.singleButtonNotify({ title: titleSuccess, text: textReload, confirm: { @@ -559,24 +624,35 @@ $(function() { closer: false, sticker: false }, + callbacks: { + before_close: beforeClose + }, hide: false }) } else { - new PNotify({ + notification = new PNotify({ title: titleSuccess, text: textSuccess, type: "success", + callbacks: { + before_close: beforeClose + }, hide: false }) } } else { - new PNotify({ + notification = new PNotify({ title: titleError, text: textError, type: "error", + callbacks: { + before_close: beforeClose + }, hide: false }); } + + self.notifications.push(notification); }; self._markWorking = function(title, line) { @@ -621,6 +697,16 @@ $(function() { self.onUserLoggedIn = function(user) { if (user.admin) { self.requestData(); + } else { + self.onUserLoggedOut(); + } + }; + + self.onUserLoggedOut = function() { + if (self.notifications) { + _.each(self.notifications, function(notification) { + notification.remove(); + }); } }; @@ -774,5 +860,9 @@ $(function() { } // view model class, parameters for constructor, container to bind to - ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]); + ADDITIONAL_VIEWMODELS.push([ + PluginManagerViewModel, + ["loginStateViewModel", "settingsViewModel", "printerStateViewModel", "systemViewModel"], + "#settings_plugin_pluginmanager" + ]); }); diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 72cbc7ca..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 %} diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 48e8e36e..4b106cdd 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -38,6 +38,7 @@ from . import log as api_logs from . import slicing as api_slicing from . import printer_profiles as api_printer_profiles from . import languages as api_languages +from . import system as api_system VERSION = "0.1" @@ -177,45 +178,6 @@ def apiVersion(): "api": VERSION }) -#~~ system control - - -@api.route("/system", methods=["POST"]) -@restricted_access -@admin_permission.require(403) -def performSystemAction(): - logger = logging.getLogger(__name__) - - data = request.values - if hasattr(request, "json") and request.json: - data = request.json - - if "action" in data: - action = data["action"] - 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: - # we run this with shell=True since we have to trust whatever - # our admin configured as command and since we want to allow - # shell-alike handling here... - 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: - if not ignore: - logger.warn("Command failed: %s" % e) - return make_response(("Command failed: %s" % e, 500, [])) - break - return NO_CONTENT - #~~ Login/user handling diff --git a/src/octoprint/server/api/system.py b/src/octoprint/server/api/system.py new file mode 100644 index 00000000..fba71f43 --- /dev/null +++ b/src/octoprint/server/api/system.py @@ -0,0 +1,176 @@ +# coding=utf-8 +from __future__ import absolute_import + +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import collections +import logging +import sarge + +from flask import request, make_response, jsonify, url_for +from flask.ext.babel import gettext + +from octoprint.settings import settings as s + +from octoprint.server import admin_permission, NO_CONTENT +from octoprint.server.api import api +from octoprint.server.util.flask import restricted_access, get_remote_address + + +@api.route("/system", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def performSystemAction(): + logging.getLogger(__name__).warn("Deprecated API call to /api/system made by {}, should be migrated to use /system/commands/custom/".format(get_remote_address(request))) + + data = request.values + if hasattr(request, "json") and request.json: + data = request.json + + if not "action" in data: + return make_response("action for perform is not defined", 400) + + return executeSystemCommand("custom", data["action"]) + + +@api.route("/system/commands", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommands(): + return jsonify(core=_to_client_specs(_get_core_command_specs()), + custom=_to_client_specs(_get_custom_command_specs())) + + +@api.route("/system/commands/", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommandsForSource(source): + if source == "core": + specs = _get_core_command_specs() + elif source == "custom": + specs = _get_custom_command_specs() + else: + return make_response("Unknown system command source: {}".format(source), 400) + + return jsonify(_to_client_specs(specs)) + + +@api.route("/system/commands//", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def executeSystemCommand(source, command): + logger = logging.getLogger(__name__) + + command_spec = _get_command_spec(source, command) + if not command_spec: + return make_response("Command {}:{} not found".format(source, command), 404) + + if not "command" in command_spec: + return make_response("Command {}:{} does not define a command to execute, can't proceed".format(source, command), 500) + + async = command_spec["async"] if "async" in command_spec else False + ignore = command_spec["ignore"] if "ignore" in command_spec else False + logger.info("Performing command for {}:{}: {}".format(source, command, command_spec["command"])) + try: + # we run this with shell=True since we have to trust whatever + # our admin configured as command and since we want to allow + # shell-alike handling here... + p = sarge.run(command_spec["command"], + stdout=sarge.Capture(), + stderr=sarge.Capture(), + shell=True, + async=async) + if not async: + if not ignore and p.returncode != 0: + returncode = p.returncode + stdout_text = p.stdout.text + stderr_text = p.stderr.text + + error = "Command failed with return code {}:\nSTDOUT: {}\nSTDERR: {}".format(returncode, stdout_text, stderr_text) + logger.warn(error) + return make_response(error, 500) + except Exception, e: + if not ignore: + error = "Command failed: {}".format(str(e)) + logger.warn(error) + return make_response(error, 500) + + return NO_CONTENT + + +def _to_client_specs(specs): + result = list() + for spec in specs.values(): + if not "action" in spec or not "source" in spec: + continue + copied = dict((k, v) for k, v in spec.items() if k in ("source", "action", "name", "confirm")) + copied["resource"] = url_for(".executeSystemCommand", + source=spec["source"], + command=spec["action"], + _external=True) + result.append(copied) + return result + + +def _get_command_spec(source, action): + if source == "core": + return _get_core_command_spec(action) + elif source == "custom": + return _get_custom_command_spec(action) + else: + return None + + +def _get_core_command_specs(): + commands = collections.OrderedDict( + shutdown=dict( + command=s().get(["server", "commands", "systemShutdownCommand"]), + name=gettext("Shutdown"), + confirm=gettext("You are about to shutdown the system.")), + reboot=dict( + command=s().get(["server", "commands", "systemRestartCommand"]), + name=gettext("Reboot"), + confirm=gettext("You are about to reboot the system.")), + restart=dict( + command=s().get(["server", "commands", "serverRestartCommand"]), + name="Restart OctoPrint", + confirm="You are about to restart the OctoPrint server.") + ) + + available_commands = dict() + for action, spec in commands.items(): + if not spec["command"]: + continue + spec.update(dict(action=action, source="core", async=True, ignore=True)) + available_commands[action] = spec + return available_commands + + +def _get_core_command_spec(action): + available_actions = _get_core_command_specs() + if not action in available_actions: + logging.getLogger(__name__).warn("Command for core action {} is not configured, you need to configure the command before it can be used".format(action)) + return None + + return available_actions[action] + + +def _get_custom_command_specs(): + specs = collections.OrderedDict() + for spec in s().get(["system", "actions"]): + if not "action" in spec: + continue + copied = dict(spec) + copied["source"] = "custom" + specs[spec["action"]] = copied + return specs + + +def _get_custom_command_spec(action): + available_actions = _get_custom_command_specs() + if not action in available_actions: + return None + + return available_actions[action] + diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 4fb4d6cb..b1c386af 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -631,6 +631,7 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"): 'js/app/viewmodels/printerprofiles.js', 'js/app/viewmodels/settings.js', 'js/app/viewmodels/slicing.js', + 'js/app/viewmodels/system.js', 'js/app/viewmodels/temperature.js', 'js/app/viewmodels/terminal.js', 'js/app/viewmodels/timelapse.js', diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index 5f301582..3e8a85c5 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -80,6 +80,26 @@ $(function() { PNotify.prototype.options.styling = "bootstrap2"; PNotify.prototype.options.mouse_reset = false; + PNotify.singleButtonNotify = function(options) { + if (!options.confirm || !options.confirm.buttons || !options.confirm.buttons.length) { + return new PNotify(options); + } + + var autoDisplay = options.auto_display != false; + + var params = $.extend(true, {}, options); + params.auto_display = false; + + var notify = new PNotify(params); + notify.options.confirm.buttons = [notify.options.confirm.buttons[0]]; + notify.modules.confirm.makeDialog(notify, notify.options.confirm); + + if (autoDisplay) { + notify.open(); + } + return notify; + }; + //~~ Initialize view models // the view model map is our basic look up table for dependencies that may be injected into other view models diff --git a/src/octoprint/static/js/app/viewmodels/navigation.js b/src/octoprint/static/js/app/viewmodels/navigation.js index 1c9851fa..37bebbe8 100644 --- a/src/octoprint/static/js/app/viewmodels/navigation.js +++ b/src/octoprint/static/js/app/viewmodels/navigation.js @@ -6,8 +6,7 @@ $(function() { self.appearance = parameters[1]; self.settings = parameters[2]; self.usersettings = parameters[3]; - - self.systemActions = self.settings.system_actions; + self.system = parameters[4]; self.appearanceClasses = ko.computed(function() { var classes = self.appearance.color(); @@ -17,36 +16,11 @@ $(function() { return classes; }); - self.triggerAction = function(action) { - var callback = function() { - OctoPrint.control.executeSystemCommand(action.action) - .done(function() { - new PNotify({title: gettext("Success"), text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"}); - }) - .fail(function() { - 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({ - message: action.confirm, - onproceed: function(e) { - callback(); - } - }); - } else { - callback(); - } - } } OCTOPRINT_VIEWMODELS.push([ NavigationViewModel, - ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"], + ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel", "systemViewModel"], "#navbar" ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/system.js b/src/octoprint/static/js/app/viewmodels/system.js new file mode 100644 index 00000000..054ceff6 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/system.js @@ -0,0 +1,98 @@ +$(function() { + function SystemViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + + self.lastCommandResponse = undefined; + self.systemActions = ko.observableArray([]); + + self.requestData = function() { + self.requestCommandData(); + }; + + self.requestCommandData = function() { + if (!self.loginState.isAdmin()) { + return; + } + + $.ajax({ + url: API_BASEURL + "system/commands", + type: "GET", + dataType: "json", + success: self.fromCommandResponse + }); + }; + + self.fromCommandResponse = function(response) { + var actions = []; + if (response.core && response.core.length) { + _.each(response.core, function(data) { + var action = _.extend({}, data); + action.actionSource = "core"; + actions.push(action); + }); + actions.push({action: "divider"}); + } + _.each(response.custom, function(data) { + var action = _.extend({}, data); + action.actionSource = "custom"; + actions.push(action); + }); + self.lastCommandResponse = response; + self.systemActions(actions); + }; + + self.triggerCommand = function(commandSpec) { + var callback = function() { + $.ajax({ + url: commandSpec.resource, + type: "POST", + dataType: "json", + data: "{}", + contentType: "application/json; charset=UTF-8", + success: function() { + new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: commandSpec.name}), type: "success"}); + }, + error: function(jqXHR, textStatus, errorThrown) { + if (!commandSpec.hasOwnProperty("ignore") || !commandSpec.ignore) { + var error = "

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

"; + error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); + new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); + } + } + }) + }; + if (commandSpec.confirm) { + showConfirmationDialog({ + message: commandSpec.confirm, + onproceed: function(e) { + callback(); + } + }); + } else { + callback(); + } + }; + + self.onUserLoggedIn = function(user) { + if (user.admin) { + self.requestData(); + } else { + self.onUserLoggedOut(); + } + }; + + self.onUserLoggedOut = function() { + self.lastCommandResponse = undefined; + self.systemActions([]); + } + } + + // view model class, parameters for constructor, container to bind to + ADDITIONAL_VIEWMODELS.push([ + SystemViewModel, + ["loginStateViewModel"], + [] + ]); +}); diff --git a/src/octoprint/templates/navbar/systemmenu.jinja2 b/src/octoprint/templates/navbar/systemmenu.jinja2 index a2ee3f3b..603d2a09 100644 --- a/src/octoprint/templates/navbar/systemmenu.jinja2 +++ b/src/octoprint/templates/navbar/systemmenu.jinja2 @@ -2,11 +2,11 @@ {{ _('System') }} -