diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index b27e14b2..ebf1ffbd 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -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() diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 43923eff..635ffc79 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -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) diff --git a/src/octoprint/cli/plugins.py b/src/octoprint/cli/plugins.py index e28c3ee6..44d60952 100644 --- a/src/octoprint/cli/plugins.py +++ b/src/octoprint/cli/plugins.py @@ -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) diff --git a/src/octoprint/cli/server.py b/src/octoprint/cli/server.py index 0b187bd6..2ea6fc4b 100644 --- a/src/octoprint/cli/server.py +++ b/src/octoprint/cli/server.py @@ -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() diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index bab1350a..31367e54 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -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, diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index c95f1326..61ba0ff7 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -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: diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index e1f36c99..97dfc456 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -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 diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 7b6ac520..c634dfe3 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -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() { diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 0c9c721d..bd717be5 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -13,8 +13,16 @@ {% endtrans %} {% endmacro %} +{% macro pluginmanager_safemode() %} +