diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index 337a4378..85ba5011 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -52,6 +52,11 @@ if version_info.major == 2 and version_info.minor <= 7 and version_info.micro < del version_info +#~~ custom exceptions + +class FatalStartupError(BaseException): + pass + #~~ init methods to bring up platform def init_platform(basedir, configfile, use_logging_file=True, logging_file=None, @@ -79,8 +84,14 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None, def init_settings(basedir, configfile): """Inits the settings instance based on basedir and configfile to use.""" - from octoprint.settings import settings - return settings(init=True, basedir=basedir, configfile=configfile) + from octoprint.settings import settings, InvalidSettings + try: + return settings(init=True, basedir=basedir, configfile=configfile) + except InvalidSettings as e: + message = "Error parsing the configuration file, it appears to be invalid YAML." + if e.line is not None and e.column is not None: + message += " The parser reported an error on line {}, column {}.".format(e.line, e.column) + raise FatalStartupError(message) def init_logging(settings, use_logging_file=True, logging_file=None, default_config=None, debug=False, verbosity=0, uncaught_logger=None, uncaught_handler=None): diff --git a/src/octoprint/cli/client.py b/src/octoprint/cli/client.py index 9615ddee..eec97b3e 100644 --- a/src/octoprint/cli/client.py +++ b/src/octoprint/cli/client.py @@ -10,7 +10,7 @@ import json import octoprint_client from octoprint.cli import pass_octoprint_ctx, bulk_options, standard_options -from octoprint import init_settings +from octoprint import init_settings, FatalStartupError class JsonStringParamType(click.ParamType): @@ -36,10 +36,16 @@ def client_commands(): @click.option("--https", is_flag=True) @click.option("--prefix", type=click.STRING) @pass_octoprint_ctx -def client(obj, host, port, httpuser, httppass, https, prefix): +@click.pass_context +def client(ctx, obj, host, port, httpuser, httppass, https, prefix): """Basic API client.""" - obj.settings = init_settings(obj.basedir, obj.configfile) - octoprint_client.init_client(obj.settings, https=https, httpuser=httpuser, httppass=httppass, host=host, port=port, prefix=prefix) + try: + obj.settings = init_settings(obj.basedir, obj.configfile) + octoprint_client.init_client(obj.settings, https=https, httpuser=httpuser, httppass=httppass, host=host, port=port, prefix=prefix) + except FatalStartupError as e: + click.echo(e.message, err=True) + click.echo("There was a fatal error initializing the client.", err=True) + ctx.exit(-1) def log_response(response, status_code=True, body=True, headers=False): diff --git a/src/octoprint/cli/plugins.py b/src/octoprint/cli/plugins.py index 47bf887a..e3dd7848 100644 --- a/src/octoprint/cli/plugins.py +++ b/src/octoprint/cli/plugins.py @@ -48,9 +48,14 @@ class OctoPrintPluginCommands(click.MultiCommand): # initialize settings and plugin manager based on provided # context (basedir and configfile) - from octoprint import init_settings, init_pluginsystem - self.settings = init_settings(ctx.obj.basedir, ctx.obj.configfile) - self.plugin_manager = init_pluginsystem(self.settings) + 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) + 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) + ctx.exit(-1) # fetch registered hooks self.hooks = self.plugin_manager.get_hooks("octoprint.cli.commands") diff --git a/src/octoprint/cli/server.py b/src/octoprint/cli/server.py index c2f3b605..d7c827c3 100644 --- a/src/octoprint/cli/server.py +++ b/src/octoprint/cli/server.py @@ -14,7 +14,7 @@ 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): """Initializes the environment and starts up the server.""" - from octoprint import init_platform, __display_version__ + from octoprint import init_platform, __display_version__, FatalStartupError def log_startup(_): logging.getLogger("octoprint.server").info("Starting OctoPrint {}".format(__display_version__)) @@ -29,17 +29,27 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi "install PyOpenSSL plus its dependencies. For details see " "https://urllib3.readthedocs.org/en/latest/security.html#openssl-pyopenssl") - settings, _, plugin_manager = init_platform(basedir, - configfile, - logging_file=logging_config, - debug=debug, - verbosity=verbosity, - uncaught_logger=__name__, - after_logging=log_startup) - - from octoprint.server import Server - octoprint_server = Server(settings=settings, plugin_manager=plugin_manager, host=host, port=port, debug=debug, allow_root=allow_root, octoprint_daemon=octoprint_daemon) - octoprint_server.run() + try: + settings, _, plugin_manager = init_platform(basedir, + configfile, + logging_file=logging_config, + debug=debug, + verbosity=verbosity, + uncaught_logger=__name__, + after_logging=log_startup) + except FatalStartupError as e: + click.echo(e.message, err=True) + click.echo("There was a fatal error starting up OctoPrint.", err=True) + else: + from octoprint.server import Server + octoprint_server = Server(settings=settings, + plugin_manager=plugin_manager, + host=host, + port=port, + debug=debug, + allow_root=allow_root, + octoprint_daemon=octoprint_daemon) + octoprint_server.run() #~~ server options diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 0a35858b..0bd354fe 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -26,6 +26,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import sys import os import yaml +import yaml.parser import logging import re import uuid @@ -345,6 +346,14 @@ class NoSuchSettingsPath(BaseException): pass +class InvalidSettings(BaseException): + def __init__(self, message, line=None, column=None, details=None): + self.message = message + self.line = line + self.column = column + self.details = details + + class HierarchicalChainMap(ChainMap): def deep_dict(self, root=None): @@ -747,8 +756,25 @@ class Settings(object): def load(self, migrate=False): if os.path.exists(self._configfile) and os.path.isfile(self._configfile): with open(self._configfile, "r") as f: - self._config = yaml.safe_load(f) - self._mtime = self.last_modified + try: + self._config = yaml.safe_load(f) + self._mtime = self.last_modified + except yaml.YAMLError as e: + details = e.message + + if hasattr(e, "problem_mark"): + line = e.problem_mark.line + column = e.problem_mark.column + else: + line = None + column = None + + raise InvalidSettings("Invalid YAML file: {}".format(self._configfile), + details=details, + line=line, + column=column) + except: + raise # changed from else to handle cases where the file exists, but is empty / 0 bytes if not self._config: self._config = dict()