Coverage for slidge / main.py: 37%
114 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-02-15 09:02 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-02-15 09:02 +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 signal
23from pathlib import Path
25import configargparse
27import slidge
28from slidge import BaseGateway
29from slidge.core import config
30from slidge.db import SlidgeStore
31from slidge.db.avatar import avatar_cache
32from slidge.db.meta import get_engine
33from slidge.migration import migrate
34from slidge.util.conf import ConfigModule
37class MainConfig(ConfigModule):
38 def update_dynamic_defaults(self, args: configargparse.Namespace) -> None:
39 # force=True is needed in case we call a logger before this is reached,
40 # or basicConfig has no effect
41 if args.log_config:
42 logging.config.fileConfig(args.log_config)
43 else:
44 logging.basicConfig(
45 level=args.loglevel,
46 filename=args.log_file,
47 force=True,
48 format=args.log_format,
49 )
51 if args.home_dir is None:
52 args.home_dir = Path("/var/lib/slidge") / str(args.jid)
54 if args.db_url is None:
55 args.db_url = f"sqlite:///{args.home_dir}/slidge.sqlite"
58class SigTermInterrupt(Exception):
59 pass
62def get_configurator(from_entrypoint: bool = False) -> MainConfig:
63 p = configargparse.ArgumentParser(
64 default_config_files=os.getenv(
65 "SLIDGE_CONF_DIR", "/etc/slidge/conf.d/*.conf"
66 ).split(":"),
67 description=__doc__,
68 )
69 p.add_argument(
70 "-c",
71 "--config",
72 help="Path to a INI config file.",
73 env_var="SLIDGE_CONFIG",
74 is_config_file=True,
75 )
76 p.add_argument(
77 "--log-config",
78 help="Path to a INI config file to personalise logging output. Refer to "
79 "<https://docs.python.org/3/library/logging.config.html#configuration-file-format> "
80 "for details.",
81 )
82 p.add_argument(
83 "-q",
84 "--quiet",
85 help="loglevel=WARNING (unused if --log-config is specified)",
86 action="store_const",
87 dest="loglevel",
88 const=logging.WARNING,
89 default=logging.INFO,
90 env_var="SLIDGE_QUIET",
91 )
92 p.add_argument(
93 "-d",
94 "--debug",
95 help="loglevel=DEBUG (unused if --log-config is specified)",
96 action="store_const",
97 dest="loglevel",
98 const=logging.DEBUG,
99 env_var="SLIDGE_DEBUG",
100 )
101 p.add_argument(
102 "--version",
103 action="version",
104 version=f"%(prog)s {slidge.__version__}",
105 )
106 configurator = MainConfig(
107 config, p, skip_options=("legacy_module",) if from_entrypoint else ()
108 )
109 return configurator
112def get_parser() -> configargparse.ArgumentParser:
113 return get_configurator().parser
116def configure(from_entrypoint: bool) -> list[str]:
117 configurator = get_configurator(from_entrypoint)
118 args, unknown_argv = configurator.set_conf()
120 if not (h := config.HOME_DIR).exists():
121 logging.info("Creating directory '%s'", h)
122 os.makedirs(h)
124 config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
126 return unknown_argv
129def handle_sigterm(_signum: int, _frame) -> None:
130 logging.info("Caught SIGTERM")
131 raise SigTermInterrupt
134def main(module_name: str | None = None) -> None:
135 from_entrypoint = module_name is not None
136 signal.signal(signal.SIGTERM, handle_sigterm)
138 unknown_argv = configure(from_entrypoint)
139 logging.info("Starting slidge version %s", slidge.__version__)
141 if module_name is not None:
142 config.LEGACY_MODULE = module_name
144 legacy_module = importlib.import_module(config.LEGACY_MODULE)
145 logging.debug("Legacy module: %s", dir(legacy_module))
146 logging.info(
147 "Starting legacy module: '%s' version %s",
148 config.LEGACY_MODULE,
149 getattr(legacy_module, "__version__", "No version"),
150 )
152 if plugin_config_obj := getattr(
153 legacy_module, "config", getattr(legacy_module, "Config", None)
154 ):
155 # If the legacy module has default parameters that depend on dynamic defaults
156 # of the slidge main config, it needs to be refreshed at this point, because
157 # now the dynamic defaults are set.
158 if inspect.ismodule(plugin_config_obj):
159 importlib.reload(plugin_config_obj)
160 logging.debug("Found a config object in plugin: %r", plugin_config_obj)
161 ConfigModule.ENV_VAR_PREFIX += (
162 f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
163 )
164 logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
165 _, unknown_argv = ConfigModule(plugin_config_obj).set_conf(unknown_argv)
167 if unknown_argv:
168 logging.error(
169 f"These config options have not been recognized and ignored: {unknown_argv}"
170 )
172 migrate()
174 gw_cls: type[BaseGateway] = BaseGateway.get_unique_subclass() # type:ignore[assignment]
175 store = SlidgeStore(
176 get_engine(
177 config.DB_URL,
178 echo=logging.getLogger().isEnabledFor(level=logging.DEBUG),
179 pool_size=gw_cls.DB_POOL_SIZE,
180 )
181 )
182 BaseGateway.store = store
183 gateway = gw_cls()
184 avatar_cache.store = gateway.store.avatars
185 avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3")
187 gateway.add_event_handler("disconnected", _on_disconnected)
188 gateway.add_event_handler("stream_error", _on_stream_error)
189 gateway.connect()
190 return_code = 0
191 try:
192 gateway.loop.run_forever()
193 except KeyboardInterrupt:
194 logging.debug("Received SIGINT")
195 except SigTermInterrupt:
196 logging.debug("Received SIGTERM")
197 except SystemExit as e:
198 return_code = e.code # type: ignore
199 logging.debug("Exit called")
200 except Exception as e:
201 return_code = 2
202 logging.exception("Exception in __main__")
203 logging.exception(e)
204 finally:
205 if gateway.has_crashed:
206 if return_code != 0:
207 logging.warning("Return code has been set twice. Please report this.")
208 return_code = 3
209 if gateway.is_connected():
210 logging.debug("Gateway is connected, cleaning up")
211 gateway.del_event_handler("disconnected", _on_disconnected)
212 gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
213 gateway.disconnect()
214 gateway.loop.run_until_complete(gateway.disconnected)
215 logging.info("Successful clean shut down")
216 else:
217 logging.debug("Gateway is not connected, no need to clean up")
218 avatar_cache.close()
219 gateway.loop.run_until_complete(gateway.http.close())
220 logging.debug("Exiting with code %s", return_code)
221 exit(return_code)
224def _on_disconnected(e):
225 logging.error("Disconnected from the XMPP server: '%s'.", e)
226 exit(10)
229def _on_stream_error(e):
230 logging.error("Stream error: '%s'.", e)