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') }}
-