Coverage for slidge/main.py: 38%
114 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
1"""
2Slidge can be configured via CLI args, environment variables and/or INI files.
4To use env vars, use this convention: ``--home-dir`` becomes ``HOME_DIR``.
6Everything in ``/etc/slidge/conf.d/*`` is automatically used.
7To use a plugin-specific INI file, put it in another dir,
8and launch slidge with ``-c /path/to/plugin-specific.conf``.
9Use the long version of the CLI arg without the double dash prefix inside this
10INI file, eg ``debug=true``.
12An example configuration file is available at
13https://codeberg.org/slidge/slidge/src/branch/main/dev/confs/slidge-example.ini
14"""
16import asyncio
17import importlib
18import inspect
19import logging
20import logging.config
21import os
22import re
23import signal
24from pathlib import Path
26import configargparse
28import slidge
29from slidge import BaseGateway
30from slidge.core import config
31from slidge.db import SlidgeStore
32from slidge.db.avatar import avatar_cache
33from slidge.db.meta import get_engine
34from slidge.migration import migrate
35from slidge.util.conf import ConfigModule
38class MainConfig(ConfigModule):
39 def update_dynamic_defaults(self, args: configargparse.Namespace) -> None:
40 # force=True is needed in case we call a logger before this is reached,
41 # or basicConfig has no effect
42 if args.log_config:
43 logging.config.fileConfig(args.log_config)
44 else:
45 logging.basicConfig(
46 level=args.loglevel,
47 filename=args.log_file,
48 force=True,
49 format=args.log_format,
50 )
52 if args.home_dir is None:
53 args.home_dir = Path("/var/lib/slidge") / str(args.jid)
55 if args.db_url is None:
56 args.db_url = f"sqlite:///{args.home_dir}/slidge.sqlite"
59class SigTermInterrupt(Exception):
60 pass
63def get_configurator(from_entrypoint: bool = False) -> MainConfig:
64 p = configargparse.ArgumentParser(
65 default_config_files=os.getenv(
66 "SLIDGE_CONF_DIR", "/etc/slidge/conf.d/*.conf"
67 ).split(":"),
68 description=__doc__,
69 )
70 p.add_argument(
71 "-c",
72 "--config",
73 help="Path to a INI config file.",
74 env_var="SLIDGE_CONFIG",
75 is_config_file=True,
76 )
77 p.add_argument(
78 "--log-config",
79 help="Path to a INI config file to personalise logging output. Refer to "
80 "<https://docs.python.org/3/library/logging.config.html#configuration-file-format> "
81 "for details.",
82 )
83 p.add_argument(
84 "-q",
85 "--quiet",
86 help="loglevel=WARNING (unused if --log-config is specified)",
87 action="store_const",
88 dest="loglevel",
89 const=logging.WARNING,
90 default=logging.INFO,
91 env_var="SLIDGE_QUIET",
92 )
93 p.add_argument(
94 "-d",
95 "--debug",
96 help="loglevel=DEBUG (unused if --log-config is specified)",
97 action="store_const",
98 dest="loglevel",
99 const=logging.DEBUG,
100 env_var="SLIDGE_DEBUG",
101 )
102 p.add_argument(
103 "--version",
104 action="version",
105 version=f"%(prog)s {slidge.__version__}",
106 )
107 configurator = MainConfig(
108 config, p, skip_options=("legacy_module",) if from_entrypoint else ()
109 )
110 return configurator
113def get_parser() -> configargparse.ArgumentParser:
114 return get_configurator().parser
117def configure(from_entrypoint: bool) -> list[str]:
118 configurator = get_configurator(from_entrypoint)
119 args, unknown_argv = configurator.set_conf()
121 if not (h := config.HOME_DIR).exists():
122 logging.info("Creating directory '%s'", h)
123 os.makedirs(h)
125 config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
127 return unknown_argv
130def handle_sigterm(_signum: int, _frame) -> None:
131 logging.info("Caught SIGTERM")
132 raise SigTermInterrupt
135def main(module_name: str | None = None) -> None:
136 from_entrypoint = module_name is not None
137 signal.signal(signal.SIGTERM, handle_sigterm)
139 unknown_argv = configure(from_entrypoint)
140 logging.info("Starting slidge version %s", slidge.__version__)
142 if module_name is not None:
143 config.LEGACY_MODULE = module_name
145 legacy_module = importlib.import_module(config.LEGACY_MODULE)
146 logging.debug("Legacy module: %s", dir(legacy_module))
147 logging.info(
148 "Starting legacy module: '%s' version %s",
149 config.LEGACY_MODULE,
150 getattr(legacy_module, "__version__", "No version"),
151 )
153 if plugin_config_obj := getattr(
154 legacy_module, "config", getattr(legacy_module, "Config", None)
155 ):
156 # If the legacy module has default parameters that depend on dynamic defaults
157 # of the slidge main config, it needs to be refreshed at this point, because
158 # now the dynamic defaults are set.
159 if inspect.ismodule(plugin_config_obj):
160 importlib.reload(plugin_config_obj)
161 logging.debug("Found a config object in plugin: %r", plugin_config_obj)
162 ConfigModule.ENV_VAR_PREFIX += (
163 f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
164 )
165 logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
166 _, unknown_argv = ConfigModule(plugin_config_obj).set_conf(unknown_argv)
168 if unknown_argv:
169 logging.error(
170 f"These config options have not been recognized and ignored: {unknown_argv}"
171 )
173 migrate()
175 store = SlidgeStore(
176 get_engine(
177 config.DB_URL, echo=logging.getLogger().isEnabledFor(level=logging.DEBUG)
178 )
179 )
180 BaseGateway.store = store
181 gateway: BaseGateway = BaseGateway.get_unique_subclass()()
182 avatar_cache.store = gateway.store.avatars
183 avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3")
185 gateway.add_event_handler("disconnected", _on_disconnected)
186 gateway.add_event_handler("stream_error", _on_stream_error)
187 gateway.connect()
188 return_code = 0
189 try:
190 gateway.loop.run_forever()
191 except KeyboardInterrupt:
192 logging.debug("Received SIGINT")
193 except SigTermInterrupt:
194 logging.debug("Received SIGTERM")
195 except SystemExit as e:
196 return_code = e.code # type: ignore
197 logging.debug("Exit called")
198 except Exception as e:
199 return_code = 2
200 logging.exception("Exception in __main__")
201 logging.exception(e)
202 finally:
203 if gateway.has_crashed:
204 if return_code != 0:
205 logging.warning("Return code has been set twice. Please report this.")
206 return_code = 3
207 if gateway.is_connected():
208 logging.debug("Gateway is connected, cleaning up")
209 gateway.del_event_handler("disconnected", _on_disconnected)
210 gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
211 gateway.disconnect()
212 gateway.loop.run_until_complete(gateway.disconnected)
213 logging.info("Successful clean shut down")
214 else:
215 logging.debug("Gateway is not connected, no need to clean up")
216 avatar_cache.close()
217 gateway.loop.run_until_complete(gateway.http.close())
218 logging.debug("Exiting with code %s", return_code)
219 exit(return_code)
222def _on_disconnected(e):
223 logging.error("Disconnected from the XMPP server: '%s'.", e)
224 exit(10)
227def _on_stream_error(e):
228 logging.error("Stream error: '%s'.", e)