Coverage for slidge/main.py: 37%
119 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 03:59 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 03:59 +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.core import config
29from slidge.core.gateway import BaseGateway
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
35from slidge.util.types import AnyGateway
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=[
66 f"{p}/*"
67 for p in os.getenv("SLIDGE_CONF_DIR", "/etc/slidge/conf.d/").split(":")
68 ],
69 description=__doc__,
70 )
71 p.add_argument(
72 "-c",
73 "--config",
74 help="Path to a INI config file.",
75 env_var="SLIDGE_CONFIG",
76 is_config_file=True,
77 )
78 p.add_argument(
79 "--log-config",
80 help="Path to a INI config file to personalise logging output. Refer to "
81 "<https://docs.python.org/3/library/logging.config.html#configuration-file-format> "
82 "for details.",
83 )
84 p.add_argument(
85 "-q",
86 "--quiet",
87 help="loglevel=WARNING (unused if --log-config is specified)",
88 action="store_const",
89 dest="loglevel",
90 const=logging.WARNING,
91 default=logging.INFO,
92 env_var="SLIDGE_QUIET",
93 )
94 p.add_argument(
95 "-d",
96 "--debug",
97 help="loglevel=DEBUG (unused if --log-config is specified)",
98 action="store_const",
99 dest="loglevel",
100 const=logging.DEBUG,
101 env_var="SLIDGE_DEBUG",
102 )
103 p.add_argument(
104 "--version",
105 action="version",
106 version=f"%(prog)s {slidge.__version__}",
107 )
108 configurator = MainConfig(
109 config, p, skip_options=("legacy_module",) if from_entrypoint else ()
110 )
111 return configurator
114def get_parser() -> configargparse.ArgumentParser:
115 return get_configurator().parser
118def configure(from_entrypoint: bool) -> list[str]:
119 configurator = get_configurator(from_entrypoint)
120 _args, unknown_argv = configurator.set_conf()
122 if not (h := config.HOME_DIR).exists():
123 logging.info("Creating directory '%s'", h)
124 os.makedirs(h)
126 config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
128 return unknown_argv
131def handle_sigterm(_signum: int, _frame: object) -> None:
132 logging.info("Caught SIGTERM")
133 raise SigTermInterrupt
136def main(module_name: str | None = None) -> None:
137 from_entrypoint = module_name is not None
138 signal.signal(signal.SIGTERM, handle_sigterm)
140 unknown_argv = configure(from_entrypoint)
141 logging.info("Starting slidge version %s", slidge.__version__)
143 if module_name is not None:
144 config.LEGACY_MODULE = module_name
146 legacy_module = importlib.import_module(config.LEGACY_MODULE)
147 logging.debug("Legacy module: %s", dir(legacy_module))
148 logging.info(
149 "Starting legacy module: '%s' version %s",
150 config.LEGACY_MODULE,
151 getattr(legacy_module, "__version__", "No version"),
152 )
154 if plugin_config_obj := getattr(
155 legacy_module, "config", getattr(legacy_module, "Config", None)
156 ):
157 # If the legacy module has default parameters that depend on dynamic defaults
158 # of the slidge main config, it needs to be refreshed at this point, because
159 # now the dynamic defaults are set.
160 if inspect.ismodule(plugin_config_obj):
161 importlib.reload(plugin_config_obj)
162 logging.debug("Found a config object in plugin: %r", plugin_config_obj)
163 ConfigModule.ENV_VAR_PREFIX += (
164 f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
165 )
166 logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
167 _, unknown_argv = ConfigModule(plugin_config_obj).set_conf(unknown_argv)
169 if unknown_argv:
170 logging.error(
171 f"These config options have not been recognized and ignored: {unknown_argv}"
172 )
174 migrate()
176 gw_cls: type[AnyGateway] = BaseGateway.get_unique_subclass() # type:ignore[assignment]
177 store = SlidgeStore(
178 get_engine(
179 config.DB_URL,
180 echo=logging.getLogger().isEnabledFor(level=logging.DEBUG),
181 )
182 )
183 BaseGateway.store = store
184 gateway = gw_cls()
185 avatar_cache.store = gateway.store.avatars
186 avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3")
188 gateway.add_event_handler("connection_lost", _on_connection_lost)
189 gateway.add_event_handler("disconnected", _on_disconnected)
190 gateway.add_event_handler("stream_error", _on_stream_error)
191 gateway.connect()
192 return_code = 0
193 try:
194 gateway.loop.run_forever()
195 except KeyboardInterrupt:
196 logging.debug("Received SIGINT")
197 except SigTermInterrupt:
198 logging.debug("Received SIGTERM")
199 except SystemExit as e:
200 return_code = e.code # type: ignore
201 logging.debug("Exit called")
202 except Exception as e:
203 return_code = 2
204 logging.exception("Exception in __main__")
205 logging.exception(e)
206 finally:
207 if gateway.has_crashed:
208 if return_code != 0:
209 logging.warning("Return code has been set twice. Please report this.")
210 return_code = 3
211 if gateway.is_connected():
212 logging.debug("Gateway is connected, cleaning up")
213 gateway.del_event_handler("disconnected", _on_disconnected)
214 gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
215 gateway.disconnect()
216 gateway.loop.run_until_complete(gateway.disconnected)
217 logging.info("Successful clean shut down")
218 else:
219 logging.debug("Gateway is not connected, no need to clean up")
220 avatar_cache.close()
221 gateway.loop.run_until_complete(gateway.http.close())
222 logging.debug("Exiting with code %s", return_code)
223 exit(return_code)
226def _on_disconnected(e: BaseException) -> None:
227 logging.error("Disconnected from the XMPP server: '%s'.", e)
228 exit(10)
231def _on_stream_error(e: BaseException) -> None:
232 logging.error("Stream error: '%s'.", e)
235def _on_connection_lost(*args: object) -> None:
236 logging.error("Connection lost: '%s'", args)
237 exit(15)