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:
parent
e4d34f4130
commit
97bf331307
13 changed files with 179 additions and 67 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"> </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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ default_settings = {
|
|||
"host": "0.0.0.0",
|
||||
"port": 5000,
|
||||
"firstRun": True,
|
||||
"startOnceInSafeMode": False,
|
||||
"seenWizards": {},
|
||||
"secretKey": None,
|
||||
"reverseProxy": {
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue