diff --git a/libervia/backend/core/main.py b/libervia/backend/core/main.py index f5ac4ea0..bf9ff768 100644 --- a/libervia/backend/core/main.py +++ b/libervia/backend/core/main.py @@ -30,10 +30,7 @@ from libervia.backend.core.i18n import _, D_, language_switch from libervia.backend.core import patches from libervia.backend.models.tor import TorData from libervia.backend.tools.config import ( - BridgeSettings, ComponentSettings, - DBusBridgeSettings, - PbBridgeSettings, ) patches.apply() @@ -111,6 +108,14 @@ class LiberviaBackend(service.Service): # reactor is run later. reactor.callLater(0, self._post_init) + # before/shutdown event is called on backend shutdown, when it's still possible to + # call Deferred. it's used to call all cleaning methods (per-profile's + # "profile_disconnected", and per-plugin's "unload"). + reactor.addSystemEventTrigger( + "before", "shutdown", lambda: defer.ensureDeferred(self._on_shutdown()) + ) + log.info("Shutdown trigger registered.") + @property def version(self): """Return the short version of Libervia""" @@ -310,8 +315,7 @@ class LiberviaBackend(service.Service): await async_process.run(str(init_script), verbose=True) except RuntimeError as e: log.error(f"Init script failed: {e}") - self.stopService() - sys.exit(C.EXIT_ERROR) + self.stop(C.EXIT_ERROR) await self._sync_components_profiles() @@ -536,14 +540,17 @@ class LiberviaBackend(service.Service): self.plugins[import_name]._info = plugin_info # TODO: test xmppclient presence and register handler parent - def plugins_unload(self): - """Call unload method on every loaded plugin, if exists + async def plugins_unload(self) -> None: + """Call ``unload`` method (if any) on every loaded plugin. - @return (D): A deferred which return None when all method have been called + Unloaded plugins are removed them from self.plugins, so ``unload`` is not called + twice. """ - # TODO: in the futur, it should be possible to hot unload a plugin - # pluging depending on the unloaded one should be unloaded too - # for now, just a basic call on plugin.unload is done + # TODO: in the futur, it should be possible to hot unload a plugin. + # Plugins depending on the unloaded one should be unloaded too; for now, + # just a basic call on plugin.unload is done. + if not self.plugins: + return defers_list = [] for plugin in self.plugins.values(): try: @@ -552,7 +559,8 @@ class LiberviaBackend(service.Service): continue else: defers_list.append(utils.as_deferred(unload)) - return defer.DeferredList(defers_list) + await defer.DeferredList(defers_list) + self.plugins.clear() async def _sync_components_profiles(self) -> None: """Synchronize components profiles data with settings. @@ -805,17 +813,25 @@ class LiberviaBackend(service.Service): self._init() log.info("Salut à toi ô mon frère !") - def stopService(self): + async def _on_shutdown(self) -> None: + """Clean up resources on shutdown. + + Disconnect all profiles first so profile_disconnected callbacks fire, then unload + plugins and storage. + """ if self.local_shared_path is not None: log.debug("Cleaning shared temporary dir.") shutil.rmtree(self.local_shared_path, ignore_errors=True) log.info("Salut aussi à Rantanplan.") - - return defer.DeferredList([self.plugins_unload(), G.storage.unload()]) - - def run(self): - log.debug(_("running app")) - reactor.run() + disconnect_defers = [ + client.entity_disconnect() + for client in self.profiles.values() + if client.is_connected() + ] + if disconnect_defers: + await defer.DeferredList(disconnect_defers) + await self.plugins_unload() + await G.storage.unload() def stop(self, exit_code: int | None = None): log.debug(_("stopping app")) diff --git a/libervia/backend/core/xmpp.py b/libervia/backend/core/xmpp.py index e66e103c..dc84cfd2 100644 --- a/libervia/backend/core/xmpp.py +++ b/libervia/backend/core/xmpp.py @@ -546,7 +546,9 @@ class SatXMPPEntity(core_types.SatXMPPEntity): # the following Deferred is used to know when we are connected # so we need to be set it to None when connection is lost self._connected_d = defer.Deferred() - self._connected_d.addCallback(self._clean_connection) + self._connected_d.addCallback( + lambda __: defer.ensureDeferred(self._clean_connection()) + ) self._connected_d.addCallback(self._disconnection_cb) self._connected_d.addErrback(self._disconnection_eb) @@ -706,8 +708,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity): return self.disconnect_profile(reason) - @defer.inlineCallbacks - def _clean_connection(self, __): + async def _clean_connection(self): """method called on disconnection used to call profile_disconnected* triggers @@ -716,7 +717,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity): for plugin in self._get_plugins_list(): disconnected_cb = getattr(plugin, trigger_name, None) if disconnected_cb is not None: - yield disconnected_cb(self) + await utils.as_deferred(disconnected_cb, self) def is_connected(self) -> bool: """Return True is client is fully connected