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