Coverage for slidge / main.py: 37%

114 statements  

« 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. 

3 

4To use env vars, use this convention: ``--home-dir`` becomes ``HOME_DIR``. 

5 

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``. 

11 

12An example configuration file is available at 

13https://codeberg.org/slidge/slidge/src/branch/main/dev/confs/slidge-example.ini 

14""" 

15 

16import asyncio 

17import importlib 

18import inspect 

19import logging 

20import logging.config 

21import os 

22import signal 

23from pathlib import Path 

24 

25import configargparse 

26 

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 

35 

36 

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 ) 

50 

51 if args.home_dir is None: 

52 args.home_dir = Path("/var/lib/slidge") / str(args.jid) 

53 

54 if args.db_url is None: 

55 args.db_url = f"sqlite:///{args.home_dir}/slidge.sqlite" 

56 

57 

58class SigTermInterrupt(Exception): 

59 pass 

60 

61 

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 

110 

111 

112def get_parser() -> configargparse.ArgumentParser: 

113 return get_configurator().parser 

114 

115 

116def configure(from_entrypoint: bool) -> list[str]: 

117 configurator = get_configurator(from_entrypoint) 

118 args, unknown_argv = configurator.set_conf() 

119 

120 if not (h := config.HOME_DIR).exists(): 

121 logging.info("Creating directory '%s'", h) 

122 os.makedirs(h) 

123 

124 config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare 

125 

126 return unknown_argv 

127 

128 

129def handle_sigterm(_signum: int, _frame) -> None: 

130 logging.info("Caught SIGTERM") 

131 raise SigTermInterrupt 

132 

133 

134def main(module_name: str | None = None) -> None: 

135 from_entrypoint = module_name is not None 

136 signal.signal(signal.SIGTERM, handle_sigterm) 

137 

138 unknown_argv = configure(from_entrypoint) 

139 logging.info("Starting slidge version %s", slidge.__version__) 

140 

141 if module_name is not None: 

142 config.LEGACY_MODULE = module_name 

143 

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 ) 

151 

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) 

166 

167 if unknown_argv: 

168 logging.error( 

169 f"These config options have not been recognized and ignored: {unknown_argv}" 

170 ) 

171 

172 migrate() 

173 

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") 

186 

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) 

222 

223 

224def _on_disconnected(e): 

225 logging.error("Disconnected from the XMPP server: '%s'.", e) 

226 exit(10) 

227 

228 

229def _on_stream_error(e): 

230 logging.error("Stream error: '%s'.", e)