diff --git a/synapse/config/_base.py b/synapse/config/_base.py
index 179aa7ff88..42364fc133 100644
--- a/synapse/config/_base.py
+++ b/synapse/config/_base.py
@@ -16,14 +16,18 @@
import argparse
import errno
+import logging
import os
from collections import OrderedDict
from hashlib import sha256
from textwrap import dedent
from typing import (
Any,
+ ClassVar,
+ Collection,
Dict,
Iterable,
+ Iterator,
List,
MutableMapping,
Optional,
@@ -40,6 +44,8 @@ import yaml
from synapse.util.templates import _create_mxc_to_http_filter, _format_ts_filter
+logger = logging.getLogger(__name__)
+
class ConfigError(Exception):
"""Represents a problem parsing the configuration
@@ -55,6 +61,38 @@ class ConfigError(Exception):
self.path = path
+def format_config_error(e: ConfigError) -> Iterator[str]:
+ """
+ Formats a config error neatly
+
+ The idea is to format the immediate error, plus the "causes" of those errors,
+ hopefully in a way that makes sense to the user. For example:
+
+ Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
+ Failed to parse config for module 'JinjaOidcMappingProvider':
+ invalid jinja template:
+ unexpected end of template, expected 'end of print statement'.
+
+ Args:
+ e: the error to be formatted
+
+ Returns: An iterator which yields string fragments to be formatted
+ """
+ yield "Error in configuration"
+
+ if e.path:
+ yield " at '%s'" % (".".join(e.path),)
+
+ yield ":\n %s" % (e.msg,)
+
+ parent_e = e.__cause__
+ indent = 1
+ while parent_e:
+ indent += 1
+ yield ":\n%s%s" % (" " * indent, str(parent_e))
+ parent_e = parent_e.__cause__
+
+
# We split these messages out to allow packages to override with package
# specific instructions.
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
@@ -119,7 +157,7 @@ class Config:
defined in subclasses.
"""
- section: str
+ section: ClassVar[str]
def __init__(self, root_config: "RootConfig" = None):
self.root = root_config
@@ -309,9 +347,12 @@ class RootConfig:
class, lower-cased and with "Config" removed.
"""
- config_classes = []
+ config_classes: List[Type[Config]] = []
+
+ def __init__(self, config_files: Collection[str] = ()):
+ # Capture absolute paths here, so we can reload config after we daemonize.
+ self.config_files = [os.path.abspath(path) for path in config_files]
- def __init__(self):
for config_class in self.config_classes:
if config_class.section is None:
raise ValueError("%r requires a section name" % (config_class,))
@@ -512,12 +553,10 @@ class RootConfig:
object from parser.parse_args(..)`
"""
- obj = cls()
-
config_args = parser.parse_args(argv)
config_files = find_config_files(search_paths=config_args.config_path)
-
+ obj = cls(config_files)
if not config_files:
parser.error("Must supply a config file.")
@@ -627,7 +666,7 @@ class RootConfig:
generate_missing_configs = config_args.generate_missing_configs
- obj = cls()
+ obj = cls(config_files)
if config_args.generate_config:
if config_args.report_stats is None:
@@ -727,6 +766,34 @@ class RootConfig:
) -> None:
self.invoke_all("generate_files", config_dict, config_dir_path)
+ def reload_config_section(self, section_name: str) -> Config:
+ """Reconstruct the given config section, leaving all others unchanged.
+
+ This works in three steps:
+
+ 1. Create a new instance of the relevant `Config` subclass.
+ 2. Call `read_config` on that instance to parse the new config.
+ 3. Replace the existing config instance with the new one.
+
+ :raises ValueError: if the given `section` does not exist.
+ :raises ConfigError: for any other problems reloading config.
+
+ :returns: the previous config object, which no longer has a reference to this
+ RootConfig.
+ """
+ existing_config: Optional[Config] = getattr(self, section_name, None)
+ if existing_config is None:
+ raise ValueError(f"Unknown config section '{section_name}'")
+ logger.info("Reloading config section '%s'", section_name)
+
+ new_config_data = read_config_files(self.config_files)
+ new_config = type(existing_config)(self)
+ new_config.read_config(new_config_data)
+ setattr(self, section_name, new_config)
+
+ existing_config.root = None
+ return existing_config
+
def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
"""Read the config files into a dict
|