diff --git a/VERSION b/VERSION index 1ebc94b..02408ef 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.22 +1.8.23 diff --git a/src/torchlight/Config.py b/src/torchlight/Config.py index 576e6d4..a0c9716 100644 --- a/src/torchlight/Config.py +++ b/src/torchlight/Config.py @@ -1,11 +1,14 @@ import json import logging import os -import sys from collections import OrderedDict from typing import Any +class ConfigError(Exception): + """A config file is missing, unreadable or contains invalid JSON.""" + + class ConfigFile: """Resolves a config file path under the config folder and parses it as JSON.""" @@ -20,10 +23,19 @@ def __init__( self.config_filepath = os.path.abspath(os.path.join(config_folder, config_filename)) def load_json(self, *, ordered: bool = False) -> Any: - with open(self.config_filepath) as fp: - if ordered: - return json.load(fp, object_pairs_hook=OrderedDict) - return json.load(fp) + try: + with open(self.config_filepath) as fp: + if ordered: + return json.load(fp, object_pairs_hook=OrderedDict) + return json.load(fp) + except FileNotFoundError as e: + raise ConfigError(f"{self.config_filepath}: config file not found") from e + except OSError as e: + raise ConfigError(f"{self.config_filepath}: cannot read config file ({e.strerror or e})") from e + except json.JSONDecodeError as e: + raise ConfigError( + f"{self.config_filepath}: invalid JSON on line {e.lineno}, column {e.colno} ({e.msg})" + ) from e class Config(ConfigFile): @@ -35,13 +47,8 @@ def __init__( super().__init__(config_folder, config_filename) self.config: dict[str, Any] = {} - def load(self) -> int: - try: - self.config = self.load_json() - except ValueError as e: - self.logger.error(sys._getframe().f_code.co_name + " " + str(e)) - return 1 - return 0 + def load(self) -> None: + self.config = self.load_json() def __getitem__(self, key: str) -> Any: if key in self.config: diff --git a/src/torchlight/Sourcemod.py b/src/torchlight/Sourcemod.py index 8c80c80..34fb64a 100644 --- a/src/torchlight/Sourcemod.py +++ b/src/torchlight/Sourcemod.py @@ -1,5 +1,4 @@ import copy -import sys from collections import OrderedDict from dataclasses import dataclass @@ -34,12 +33,8 @@ def __init__( self.sm_flags: OrderedDict = OrderedDict() self.sm_groups: list[SourcemodGroup] = [] - def Load(self) -> int: - try: - self.sm_flags = self.load_json(ordered=True) - except ValueError as e: - self.logger.error(sys._getframe().f_code.co_name + " " + str(e)) - return 1 + def Load(self) -> None: + self.sm_flags = self.load_json(ordered=True) self.sm_groups.clear() for sm_group in self.config["SourcemodGroups"]: self.sm_groups.append( @@ -49,7 +44,6 @@ def Load(self) -> int: flags=sm_group["flags"], ) ) - return 0 def flagbits_to_flags(self, *, flagbits: int) -> list[str]: flags: list[str] = [] diff --git a/src/torchlight/cli.py b/src/torchlight/cli.py index 1ca15b5..03e5b20 100644 --- a/src/torchlight/cli.py +++ b/src/torchlight/cli.py @@ -6,7 +6,7 @@ import click -from torchlight.Config import Config +from torchlight.Config import Config, ConfigError from torchlight.PlayerManager import PlayerManager from torchlight.SourceRCONServer import SourceRCONServer from torchlight.TorchlightHandler import TorchlightHandler @@ -30,7 +30,13 @@ def graceful_shutdown(signal: int, frame: FrameType | None) -> None: @click.version_option() def cli(config_folder: str) -> None: config = Config(config_folder) - config.load() + + # A malformed or missing config file aborts startup with the offending path and + # parse error rather than silently starting with an empty config (see issue #164). + try: + config.load() + except ConfigError as e: + raise click.ClickException(str(e)) from e logging.basicConfig( level=logging.getLevelName(config["Logging"]["level"]), @@ -44,7 +50,10 @@ def cli(config_folder: str) -> None: event_loop = asyncio.get_event_loop() global torchlight_handler - torchlight_handler = TorchlightHandler(event_loop, config) + try: + torchlight_handler = TorchlightHandler(event_loop, config) + except ConfigError as e: + raise click.ClickException(str(e)) from e # Handles new connections on 0.0.0.0:27015 rcon_server = SourceRCONServer(