16
16
17
17
import argparse
18
18
import errno
19
+ import logging
19
20
import os
20
21
from collections import OrderedDict
21
22
from hashlib import sha256
22
23
from textwrap import dedent
23
24
from typing import (
24
25
Any ,
26
+ ClassVar ,
27
+ Collection ,
25
28
Dict ,
26
29
Iterable ,
30
+ Iterator ,
27
31
List ,
28
32
MutableMapping ,
29
33
Optional ,
40
44
41
45
from synapse .util .templates import _create_mxc_to_http_filter , _format_ts_filter
42
46
47
+ logger = logging .getLogger (__name__ )
48
+
43
49
44
50
class ConfigError (Exception ):
45
51
"""Represents a problem parsing the configuration
@@ -55,6 +61,38 @@ def __init__(self, msg: str, path: Optional[Iterable[str]] = None):
55
61
self .path = path
56
62
57
63
64
+ def format_config_error (e : ConfigError ) -> Iterator [str ]:
65
+ """
66
+ Formats a config error neatly
67
+
68
+ The idea is to format the immediate error, plus the "causes" of those errors,
69
+ hopefully in a way that makes sense to the user. For example:
70
+
71
+ Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
72
+ Failed to parse config for module 'JinjaOidcMappingProvider':
73
+ invalid jinja template:
74
+ unexpected end of template, expected 'end of print statement'.
75
+
76
+ Args:
77
+ e: the error to be formatted
78
+
79
+ Returns: An iterator which yields string fragments to be formatted
80
+ """
81
+ yield "Error in configuration"
82
+
83
+ if e .path :
84
+ yield " at '%s'" % ("." .join (e .path ),)
85
+
86
+ yield ":\n %s" % (e .msg ,)
87
+
88
+ parent_e = e .__cause__
89
+ indent = 1
90
+ while parent_e :
91
+ indent += 1
92
+ yield ":\n %s%s" % (" " * indent , str (parent_e ))
93
+ parent_e = parent_e .__cause__
94
+
95
+
58
96
# We split these messages out to allow packages to override with package
59
97
# specific instructions.
60
98
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
@@ -119,7 +157,7 @@ class Config:
119
157
defined in subclasses.
120
158
"""
121
159
122
- section : str
160
+ section : ClassVar [ str ]
123
161
124
162
def __init__ (self , root_config : "RootConfig" = None ):
125
163
self .root = root_config
@@ -309,9 +347,12 @@ class RootConfig:
309
347
class, lower-cased and with "Config" removed.
310
348
"""
311
349
312
- config_classes = []
350
+ config_classes : List [Type [Config ]] = []
351
+
352
+ def __init__ (self , config_files : Collection [str ] = ()):
353
+ # Capture absolute paths here, so we can reload config after we daemonize.
354
+ self .config_files = [os .path .abspath (path ) for path in config_files ]
313
355
314
- def __init__ (self ):
315
356
for config_class in self .config_classes :
316
357
if config_class .section is None :
317
358
raise ValueError ("%r requires a section name" % (config_class ,))
@@ -512,12 +553,10 @@ def load_config_with_parser(
512
553
object from parser.parse_args(..)`
513
554
"""
514
555
515
- obj = cls ()
516
-
517
556
config_args = parser .parse_args (argv )
518
557
519
558
config_files = find_config_files (search_paths = config_args .config_path )
520
-
559
+ obj = cls ( config_files )
521
560
if not config_files :
522
561
parser .error ("Must supply a config file." )
523
562
@@ -627,7 +666,7 @@ def load_or_generate_config(
627
666
628
667
generate_missing_configs = config_args .generate_missing_configs
629
668
630
- obj = cls ()
669
+ obj = cls (config_files )
631
670
632
671
if config_args .generate_config :
633
672
if config_args .report_stats is None :
@@ -727,6 +766,34 @@ def generate_missing_files(
727
766
) -> None :
728
767
self .invoke_all ("generate_files" , config_dict , config_dir_path )
729
768
769
+ def reload_config_section (self , section_name : str ) -> Config :
770
+ """Reconstruct the given config section, leaving all others unchanged.
771
+
772
+ This works in three steps:
773
+
774
+ 1. Create a new instance of the relevant `Config` subclass.
775
+ 2. Call `read_config` on that instance to parse the new config.
776
+ 3. Replace the existing config instance with the new one.
777
+
778
+ :raises ValueError: if the given `section` does not exist.
779
+ :raises ConfigError: for any other problems reloading config.
780
+
781
+ :returns: the previous config object, which no longer has a reference to this
782
+ RootConfig.
783
+ """
784
+ existing_config : Optional [Config ] = getattr (self , section_name , None )
785
+ if existing_config is None :
786
+ raise ValueError (f"Unknown config section '{ section_name } '" )
787
+ logger .info ("Reloading config section '%s'" , section_name )
788
+
789
+ new_config_data = read_config_files (self .config_files )
790
+ new_config = type (existing_config )(self )
791
+ new_config .read_config (new_config_data )
792
+ setattr (self , section_name , new_config )
793
+
794
+ existing_config .root = None
795
+ return existing_config
796
+
730
797
731
798
def read_config_files (config_files : Iterable [str ]) -> Dict [str , Any ]:
732
799
"""Read the config files into a dict
0 commit comments