Add safe mode that disables all third party plugins

Can be enabled either through new --safe command line
parameter or through server.startOnceInSafeMode in
config.yaml

When running in safe mode the plugin manager will
only allow to disable or uninstall third party plugins. Enabling
third party plugins or installing new plugins is disabled.

That will hopefully allow for more straightforward recovery
in case of a misbehaving plugin.
This commit is contained in:
Gina Häußge 2016-11-18 13:01:14 +01:00
parent e4d34f4130
commit 97bf331307
13 changed files with 179 additions and 67 deletions

View file

@ -57,16 +57,21 @@ class FatalStartupError(BaseException):
def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
logging_config=None, debug=False, verbosity=0, uncaught_logger=None,
uncaught_handler=None, after_preinit_logging=None,
after_settings=None, after_logging=None):
uncaught_handler=None, safe_mode=False, after_preinit_logging=None,
after_settings=None, after_logging=None, after_safe_mode=None):
kwargs = dict()
logger, recorder = preinit_logging(debug, verbosity, uncaught_logger, uncaught_handler)
kwargs["logger"] = logger
kwargs["recorder"] = recorder
if callable(after_preinit_logging):
after_preinit_logging(logger, recorder)
after_preinit_logging(**kwargs)
settings = init_settings(basedir, configfile)
kwargs["settings"] = settings
if callable(after_settings):
after_settings(settings)
after_settings(**kwargs)
logger = init_logging(settings,
use_logging_file=use_logging_file,
@ -76,12 +81,20 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None,
verbosity=verbosity,
uncaught_logger=uncaught_logger,
uncaught_handler=uncaught_handler)
kwargs["logger"] = logger
if callable(after_logging):
after_logging(logger, recorder)
after_logging(**kwargs)
plugin_manager = init_pluginsystem(settings)
return settings, logger, plugin_manager
settings_safe_mode = settings.getBoolean(["server", "startOnceInSafeMode"])
safe_mode = safe_mode or settings_safe_mode
kwargs["safe_mode"] = safe_mode
if callable(after_safe_mode):
after_safe_mode(**kwargs)
plugin_manager = init_pluginsystem(settings, safe_mode=safe_mode)
return settings, logger, safe_mode, plugin_manager
def init_settings(basedir, configfile):
@ -254,19 +267,45 @@ def set_logging_config(config, debug, verbosity, uncaught_logger, uncaught_handl
if uncaught_handler is None:
def exception_logger(exc_type, exc_value, exc_tb):
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
uncaught_handler = exception_logger
sys.excepthook = uncaught_handler
return logger
def init_pluginsystem(settings):
def init_pluginsystem(settings, safe_mode=False):
"""Initializes the plugin manager based on the settings."""
from octoprint.plugin import plugin_manager
pm = plugin_manager(init=True, settings=settings)
import os
logger = log.getLogger(__name__)
plugin_folders = [(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")), True),
settings.getBaseFolder("plugins")]
plugin_entry_points = ["octoprint.plugin"]
plugin_disabled_list = settings.get(["plugins", "_disabled"])
plugin_validators = []
if safe_mode:
def validator(phase, plugin_info):
if phase == "after_load":
setattr(plugin_info, "safe_mode_victim", not plugin_info.bundled)
setattr(plugin_info, "safe_mode_enabled", False)
elif phase == "before_enable":
if not plugin_info.bundled:
setattr(plugin_info, "safe_mode_enabled", True)
return False
return True
plugin_validators.append(validator)
from octoprint.plugin import plugin_manager
pm = plugin_manager(init=True,
plugin_folders=plugin_folders,
plugin_entry_points=plugin_entry_points,
plugin_disabled_list=plugin_disabled_list,
plugin_validators=plugin_validators)
settings_overlays = dict()
disabled_from_overlays = dict()

View file

@ -13,10 +13,11 @@ import octoprint
class OctoPrintContext(object):
"""Custom context wrapping the standard options."""
def __init__(self, configfile=None, basedir=None, verbosity=0):
def __init__(self, configfile=None, basedir=None, verbosity=0, safe_mode=False):
self.configfile = configfile
self.basedir = basedir
self.verbosity = verbosity
self.safe_mode = safe_mode
pass_octoprint_ctx = click.make_pass_decorator(OctoPrintContext, ensure=True)
"""Decorator to pass in the :class:`OctoPrintContext` instance."""
@ -95,6 +96,8 @@ def standard_options(hidden=False):
help="Specify the config file to use."),
factory("--verbose", "-v", "verbosity", count=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False,
help="Increase logging verbosity"),
factory("--safe", "safe_mode", is_flag=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False,
help="Enable safe mode; disables all third party plugins")
]
return bulk_options(options)

View file

@ -51,7 +51,7 @@ class OctoPrintPluginCommands(click.MultiCommand):
from octoprint import init_settings, init_pluginsystem, FatalStartupError
try:
self.settings = init_settings(ctx.obj.basedir, ctx.obj.configfile)
self.plugin_manager = init_pluginsystem(self.settings)
self.plugin_manager = init_pluginsystem(self.settings, safe_mode=ctx.obj.safe_mode)
except FatalStartupError as e:
click.echo(e.message, err=True)
click.echo("There was a fatal error initializing the settings or the plugin system.", err=True)

View file

@ -11,14 +11,16 @@ import sys
from octoprint.cli import pass_octoprint_ctx, bulk_options, standard_options
def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, octoprint_daemon = None):
def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode, octoprint_daemon=None):
"""Initializes the environment and starts up the server."""
from octoprint import init_platform, __display_version__, FatalStartupError
def log_startup(_, recorder):
def log_startup(recorder=None, safe_mode=None, **kwargs):
logger = logging.getLogger("octoprint.server")
logger.info("Starting OctoPrint {}".format(__display_version__))
if safe_mode:
logger.info("Starting in SAFE MODE. Third party plugins will be disabled!")
if recorder and len(recorder):
logger.info("--- Logged during platform initialization: ---")
@ -41,13 +43,14 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
"https://urllib3.readthedocs.org/en/latest/security.html#openssl-pyopenssl")
try:
settings, _, plugin_manager = init_platform(basedir,
configfile,
logging_file=logging_config,
debug=debug,
verbosity=verbosity,
uncaught_logger=__name__,
after_logging=log_startup)
settings, _, safe_mode, plugin_manager = init_platform(basedir,
configfile,
logging_file=logging_config,
debug=debug,
verbosity=verbosity,
uncaught_logger=__name__,
safe_mode=safe_mode,
after_safe_mode=log_startup)
except FatalStartupError as e:
click.echo(e.message, err=True)
click.echo("There was a fatal error starting up OctoPrint.", err=True)
@ -58,6 +61,7 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi
host=host,
port=port,
debug=debug,
safe_mode=safe_mode,
allow_root=allow_root,
octoprint_daemon=octoprint_daemon)
octoprint_server.run()
@ -73,7 +77,7 @@ server_options = bulk_options([
help="Specify the config file to use for configuring logging."),
click.option("--iknowwhatimdoing", "allow_root", is_flag=True,
help="Allow OctoPrint to run as user root."),
click.option("--debug", is_flag=True, help="Enable debug mode"),
click.option("--debug", is_flag=True, help="Enable debug mode.")
])
"""Decorator to add the options shared among the server commands: ``--host``, ``--port``,
``--logging``, ``--iknowwhatimdoing`` and ``--debug``."""
@ -93,7 +97,7 @@ def server_commands(obj):
def serve_command(obj, host, port, logging, allow_root, debug):
"""Starts the OctoPrint server."""
run_server(obj.basedir, obj.configfile, host, port, debug,
allow_root, logging, obj.verbosity)
allow_root, logging, obj.verbosity, obj.safe_mode)
@server_commands.command(name="daemon")
@ -122,7 +126,7 @@ def daemon_command(octoprint_ctx, pid, host, port, logging, allow_root, debug, c
from octoprint.daemon import Daemon
class OctoPrintDaemon(Daemon):
def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity):
def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode):
Daemon.__init__(self, pidfile)
self._basedir = basedir
@ -133,12 +137,16 @@ def daemon_command(octoprint_ctx, pid, host, port, logging, allow_root, debug, c
self._allow_root = allow_root
self._logging_config = logging_config
self._verbosity = verbosity
self._safe_mode = safe_mode
def run(self):
run_server(self._basedir, self._configfile, self._host, self._port, self._debug, self._allow_root, self._logging_config, self._verbosity, self)
run_server(self._basedir, self._configfile, self._host, self._port, self._debug,
self._allow_root, self._logging_config, self._verbosity, self._safe_mode,
octoprint_daemon=self)
octoprint_daemon = OctoPrintDaemon(pid, octoprint_ctx.basedir, octoprint_ctx.configfile,
host, port, debug, allow_root, logging, octoprint_ctx.verbosity)
host, port, debug, allow_root, logging, octoprint_ctx.verbosity,
octoprint_ctx.safe_mode)
if command == "start":
octoprint_daemon.start()

View file

@ -42,9 +42,10 @@ def _validate_plugin(phase, plugin_info):
if not "octoprint.accesscontrol.appkey" in hooks:
hooks["octoprint.accesscontrol.appkey"] = plugin_info.implementation.get_additional_apps
setattr(plugin_info.instance, PluginInfo.attr_hooks, hooks)
return True
def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None,
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None, settings=None):
plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None):
"""
Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager`
singleton.
@ -87,14 +88,6 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
else:
if init:
if settings is None:
settings = s()
if plugin_folders is None:
plugin_folders = (
settings.getBaseFolder("plugins"),
(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")), True)
)
if plugin_types is None:
plugin_types = [StartupPlugin,
ShutdownPlugin,
@ -109,22 +102,17 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en
ProgressPlugin,
WizardPlugin,
UiPlugin]
if plugin_entry_points is None:
plugin_entry_points = "octoprint.plugin"
if plugin_disabled_list is None:
plugin_disabled_list = settings.get(["plugins", "_disabled"])
if plugin_restart_needing_hooks is None:
plugin_restart_needing_hooks = [
"octoprint.server.http"
]
plugin_restart_needing_hooks = ["octoprint.server.http"]
if plugin_obsolete_hooks is None:
plugin_obsolete_hooks = [
"octoprint.comm.protocol.gcode"
]
plugin_obsolete_hooks = ["octoprint.comm.protocol.gcode"]
if plugin_validators is None:
plugin_validators = [
_validate_plugin
]
plugin_validators = [_validate_plugin]
else:
plugin_validators.append(_validate_plugin)
_instance = PluginManager(plugin_folders,
plugin_types,

View file

@ -149,6 +149,8 @@ class PluginInfo(object):
self._license = license
def validate(self, phase, additional_validators=None):
result = True
if phase == "before_load":
# if the plugin still uses __plugin_init__, log a deprecation warning and move it to __plugin_load__
if hasattr(self.instance, self.__class__.attr_init):
@ -183,7 +185,9 @@ class PluginInfo(object):
if additional_validators is not None:
for validator in additional_validators:
validator(phase, self)
result = result and validator(phase, self)
return result
def __str__(self):
if self.version:
@ -448,6 +452,12 @@ class PluginManager(object):
if logging_prefix is None:
logging_prefix = ""
if plugin_folders is None:
plugin_folders = []
if plugin_types is None:
plugin_types = []
if plugin_entry_points is None:
plugin_entry_points = []
if plugin_disabled_list is None:
plugin_disabled_list = []
@ -740,7 +750,9 @@ class PluginManager(object):
plugin = self.plugins[name]
try:
plugin.validate("before_load", additional_validators=self.plugin_validators)
if not plugin.validate("before_load", additional_validators=self.plugin_validators):
return
plugin.load()
plugin.validate("after_load", additional_validators=self.plugin_validators)
self.on_plugin_loaded(name, plugin)
@ -802,6 +814,9 @@ class PluginManager(object):
raise PluginCantEnable(name, "Dependency on obsolete hooks detected, full functionality cannot be guaranteed")
try:
if not plugin.validate("before_enable", additional_validators=self.plugin_validators):
return False
plugin.enable()
self._activate_plugin(name, plugin)
except PluginLifecycleException as e:

View file

@ -180,6 +180,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
if not admin_permission.can():
return make_response("Insufficient rights", 403)
from octoprint.server import safe_mode
refresh_repository = request.values.get("refresh_repository", "false") in valid_boolean_trues
if refresh_repository:
self._repository_available = self._refresh_repository()
@ -200,7 +202,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
virtual_env=self._pip_caller.virtual_env,
additional_args=self._settings.get(["pip_args"]),
python=sys.executable
))
),
safe_mode=safe_mode)
def etag():
import hashlib
@ -208,6 +211,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
hash.update(repr(self._get_plugins()))
hash.update(str(self._repository_available))
hash.update(repr(self._repository_plugins))
hash.update(repr(safe_mode))
return hash.hexdigest()
def condition():
@ -450,7 +454,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin)
pending = ((command == "disable" and plugin.key in self._pending_enable) or (command == "enable" and plugin.key in self._pending_disable))
needs_restart_api = needs_restart and not pending
safe_mode_victim = getattr(plugin, "safe_mode_victim", False)
needs_restart_api = (needs_restart or safe_mode_victim) and not pending
needs_refresh_api = needs_refresh and not pending
try:
@ -514,12 +519,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
self._settings.global_set(["plugins", "_disabled"], disabled_list)
self._settings.save(force=True)
if not needs_restart:
if not needs_restart and not getattr(plugin, "safe_mode_victim", False):
self._plugin_manager.enable_plugin(plugin.key)
else:
if plugin.key in self._pending_disable:
self._pending_disable.remove(plugin.key)
elif not plugin.enabled and plugin.key not in self._pending_enable:
elif (not plugin.enabled and not getattr(plugin, "safe_mode_enabled", False)) and plugin.key not in self._pending_enable:
self._pending_enable.add(plugin.key)
def _mark_plugin_disabled(self, plugin, needs_restart=False):
@ -529,12 +534,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
self._settings.global_set(["plugins", "_disabled"], disabled_list)
self._settings.save(force=True)
if not needs_restart:
if not needs_restart and not getattr(plugin, "safe_mode_victim", False):
self._plugin_manager.disable_plugin(plugin.key)
else:
if plugin.key in self._pending_enable:
self._pending_enable.remove(plugin.key)
elif plugin.enabled and plugin.key not in self._pending_disable:
elif (plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key not in self._pending_disable:
self._pending_disable.add(plugin.key)
def _fetch_repository_from_disk(self):
@ -688,8 +693,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
bundled=plugin.bundled,
managable=plugin.managable,
enabled=plugin.enabled,
pending_enable=(not plugin.enabled and plugin.key in self._pending_enable),
pending_disable=(plugin.enabled and plugin.key in self._pending_disable),
safe_mode_victim=getattr(plugin, "safe_mode_victim", False),
safe_mode_enabled=getattr(plugin, "safe_mode_enabled", False),
pending_enable=(not plugin.enabled and not getattr(plugin, "safe_mode_enabled", False) and plugin.key in self._pending_enable),
pending_disable=((plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key in self._pending_disable),
pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")),
pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")),
origin=plugin.origin.type

View file

@ -155,6 +155,8 @@ $(function() {
self.pipAdditionalArgs = ko.observable();
self.pipPython = ko.observable();
self.safeMode = ko.observable();
self.pipUseUserString = ko.pureComputed(function() {
return self.pipUseUser() ? "yes" : "no";
});
@ -186,7 +188,8 @@ $(function() {
});
self.enableToggle = function(data) {
return self.enableManagement() && data.key != 'pluginmanager';
var command = self._getToggleCommand(data);
return self.enableManagement() && (command == "disable" || !data.safe_mode_victim || data.safe_mode_enabled) && data.key != 'pluginmanager';
};
self.enableUninstall = function(data) {
@ -199,7 +202,7 @@ $(function() {
};
self.enableRepoInstall = function(data) {
return self.enableManagement() && self.pipAvailable() && self.isCompatible(data);
return self.enableManagement() && self.pipAvailable() && !self.safeMode() && self.isCompatible(data);
};
self.invalidUrl = ko.pureComputed(function() {
@ -209,7 +212,7 @@ $(function() {
self.enableUrlInstall = ko.pureComputed(function() {
var url = self.installUrl();
return self.enableManagement() && self.pipAvailable() && url !== undefined && url.trim() != "" && !self.invalidUrl();
return self.enableManagement() && self.pipAvailable() && !self.safeMode() && url !== undefined && url.trim() != "" && !self.invalidUrl();
});
self.invalidArchive = ko.pureComputed(function() {
@ -219,7 +222,7 @@ $(function() {
self.enableArchiveInstall = ko.pureComputed(function() {
var name = self.uploadFilename();
return self.enableManagement() && self.pipAvailable() && name !== undefined && name.trim() != "" && !self.invalidArchive();
return self.enableManagement() && self.pipAvailable() && !self.safeMode() && name !== undefined && name.trim() != "" && !self.invalidArchive();
});
self.uploadElement.fileupload({
@ -278,6 +281,8 @@ $(function() {
self._fromPluginsResponse(data.plugins);
self._fromRepositoryResponse(data.repository);
self._fromPipResponse(data.pip);
self.safeMode(data.safe_mode || false);
};
self._fromPluginsResponse = function(data) {
@ -347,6 +352,7 @@ $(function() {
};
if (self._getToggleCommand(data) == "enable") {
if (data.safe_mode_victim && !data.safe_mode_enabled) return;
OctoPrint.plugins.pluginmanager.enable(data.key)
.done(onSuccess)
.fail(onError);
@ -655,7 +661,8 @@ $(function() {
};
self._getToggleCommand = function(data) {
return ((!data.enabled || data.pending_disable) && !data.pending_enable) ? "enable" : "disable";
var disable = (data.enabled || data.pending_enable || (data.safe_mode_victim && data.safe_mode_enabled)) && !data.pending_disable;
return disable ? "disable" : "enable";
};
self.toggleButtonCss = function(data) {
@ -666,7 +673,16 @@ $(function() {
};
self.toggleButtonTitle = function(data) {
return self._getToggleCommand(data) == "enable" ? gettext("Enable Plugin") : gettext("Disable Plugin");
var command = self._getToggleCommand(data);
if (command == "enable") {
if (data.safe_mode_victim && !data.safe_mode_enabled) {
return gettext("Disabled due to active safe mode");
} else {
return gettext("Enable Plugin");
}
} else {
return gettext("Disable Plugin");
}
};
self.onBeforeBinding = function() {

View file

@ -13,8 +13,16 @@
{% endtrans %}</div>
{% endmacro %}
{% macro pluginmanager_safemode() %}
<div class="alert" data-bind="visible: safeMode">{% trans %}
Safe mode is currently active. All third party plugins are disabled and cannot be
enabled. Installation of plugin packages is disabled.
{% endtrans %}</div>
{% endmacro %}
{{ pluginmanager_printing() }}
{{ pluginmanager_nopip() }}
{{ pluginmanager_safemode() }}
<div class="pull-right">
<button class="btn btn-small" data-bind="click: function() { $root.showPluginSettings(); }" title="{{ _('Plugin Configuration') }}"><i class="icon-wrench"></i></button>
@ -32,7 +40,7 @@
<tbody data-bind="foreach: plugins.paginatedItems">
<tr>
<td class="settings_plugin_plugin_manager_plugins_name">
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i></div>
<div data-bind="css: {muted: !enabled}"><span data-bind="text: name"></span> <span data-bind="visible: version">(<span data-bind="text: version"></span>)</span> <i title="{{ _('Bundled with OctoPrint') }}" class="icon-th-large" data-bind="visible: bundled"></i> <i class="icon-lock" title="{{ _('Cannot be uninstalled through OctoPrint') }}" data-bind="visible: !managable"></i> <i title="{{ _('Restart of OctoPrint needed for changes to take effect') }}" class="icon-refresh" data-bind="visible: pending_enable || pending_disable || pending_install || pending_uninstall"></i> <i title="{{ _('Pending install') }}" class="icon-plus" data-bind="visible: pending_install"></i> <i title="{{ _('Pending uninstall') }}" class="icon-minus" data-bind="visible: pending_uninstall"></i> <i title="{{ _('Disabled due to safe mode') }}" class="icon-medkit" data-bind="visible: safe_mode_victim"></i></div>
<div><small class="muted" data-bind="text: description">&nbsp;</small></div>
<div data-bind="css: {muted: !enabled}">
<small data-bind="visible: url"><i class="icon-home"></i> <a data-bind="attr: {href: url}" target="_blank" rel="noreferrer noopener">{{ _('Homepage') }}</a></small>
@ -99,6 +107,7 @@
<div class="modal-body">
{{ pluginmanager_printing() }}
{{ pluginmanager_nopip() }}
{{ pluginmanager_safemode() }}
<h4 style="position: relative">
{{ _('... from the <a href="%(url)s" target="_blank">Plugin Repository</a>', url='http://plugins.octoprint.org') }}
<div class="dropdown pull-right">

View file

@ -33,6 +33,7 @@ app = Flask("octoprint")
assets = None
babel = None
debug = False
safe_mode = False
printer = None
printerProfileManager = None
@ -112,12 +113,13 @@ def load_user(id):
class Server(object):
def __init__(self, settings=None, plugin_manager=None, host="0.0.0.0", port=5000, debug=False, allow_root=False, octoprint_daemon=None):
def __init__(self, settings=None, plugin_manager=None, host="0.0.0.0", port=5000, debug=False, safe_mode=False, allow_root=False, octoprint_daemon=None):
self._settings = settings
self._plugin_manager = plugin_manager
self._host = host
self._port = port
self._debug = debug
self._safe_mode = safe_mode
self._allow_root = allow_root
self._octoprint_daemon = octoprint_daemon
self._server = None
@ -155,11 +157,13 @@ class Server(object):
global pluginLifecycleManager
global preemptiveCache
global debug
global safe_mode
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
debug = self._debug
safe_mode = self._safe_mode
self._logger = logging.getLogger(__name__)
pluginManager = self._plugin_manager
@ -495,6 +499,11 @@ class Server(object):
def on_after_startup():
self._logger.info("Listening on http://%s:%d" % (self._host, self._port))
if safe_mode and self._settings.getBoolean(["server", "startOnceInSafeMode"]):
self._logger.info("Server started successfully in safe mode as requested from config, removing flag")
self._settings.setBoolean(["server", "startOnceInSafeMode"], False)
self._settings.save()
# now this is somewhat ugly, but the issue is the following: startup plugins might want to do things for
# which they need the server to be already alive (e.g. for being able to resolve urls, such as favicons
# or service xmls or the like). While they are working though the ioloop would block. Therefore we'll

View file

@ -77,7 +77,9 @@ class PrinterStateConnection(sockjs.tornado.SockJSConnection, octoprint.printer.
display_version=octoprint.server.DISPLAY_VERSION,
branch=octoprint.server.BRANCH,
plugin_hash=plugin_hash.hexdigest(),
config_hash=config_hash
config_hash=config_hash,
debug=octoprint.server.debug,
safe_mode=octoprint.server.safe_mode
))
self._printer.register_callback(self)

View file

@ -118,6 +118,7 @@ default_settings = {
"host": "0.0.0.0",
"port": 5000,
"firstRun": True,
"startOnceInSafeMode": False,
"seenWizards": {},
"secretKey": None,
"reverseProxy": {

View file

@ -13,6 +13,8 @@ function DataUpdater(allViewModels) {
self._lastProcessingTimes = [];
self._lastProcessingTimesSize = 20;
self._safeModePopup = undefined;
self.increaseThrottle = function() {
self.setThrottle(self._throttleFactor + 1);
};
@ -115,6 +117,19 @@ function DataUpdater(allViewModels) {
var oldConfigHash = self._configHash;
self._configHash = data["config_hash"];
// process safe mode
if (self._safeModePopup) self._safeModePopup.remove();
if (data["safe_mode"]) {
// safe mode is active, let's inform the user
log.info("Safe mode is active. Third party plugins are disabled and cannot be enabled.");
self._safeModePopup = new PNotify({
title: gettext("Safe mode is active"),
text: gettext("The server is currently running in safe mode. Third party plugins are disabled and cannot be enabled."),
hide: false
});
}
// if the offline overlay is still showing, now's a good time to
// hide it, plus reload the camera feed if it's currently displayed
if ($("#offline_overlay").is(":visible")) {