# HG changeset patch # User Goffi # Date 1677951437 -3600 # Node ID a169cbc315f085660d535892ad31b13715f3e222 # Parent 409d10211b202f4673bb3bda622fd226f9a59d15 server: don't wait anymore for libervia app to be fully started: following change in backend, libervia app are started and the loading workflow continue immediately, the proxy is created only when the app is known to be actually started (through the `application_started` signal or a flag received when starting the application). This avoid stopping the loading of website for a long time, or breaking when a timeout is reached. diff -r 409d10211b20 -r a169cbc315f0 libervia/server/server.py --- a/libervia/server/server.py Wed Mar 01 18:02:44 2023 +0100 +++ b/libervia/server/server.py Sat Mar 04 18:37:17 2023 +0100 @@ -16,55 +16,55 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from functools import partial +import os.path +from pathlib import Path import re -import os.path import sys -import urllib.parse -import urllib.request, urllib.error import time -import copy -from typing import Optional -from pathlib import Path +from typing import Dict, Optional, Callable +import urllib.error +import urllib.parse +import urllib.request + +from sat.core import exceptions +from sat.core.i18n import D_, _ +from sat.core.log import getLogger +from sat.tools import utils +from sat.tools import config +from sat.tools.common import regex +from sat.tools.common import template +from sat.tools.common import uri as common_uri +from sat.tools.common import data_format +from sat.tools.common import tls +from sat.tools.common.utils import OrderedSet, recursive_update +from sat_frontends.bridge.bridge_frontend import BridgeException +from sat_frontends.bridge.dbus_bridge import ( + Bridge, + BridgeExceptionNoService, + const_TIMEOUT as BRIDGE_TIMEOUT, +) from twisted.application import service -from twisted.internet import reactor, defer, inotify +from twisted.internet import defer, inotify, reactor +from twisted.python import failure +from twisted.python import filepath +from twisted.python.components import registerAdapter from twisted.web import server from twisted.web import static from twisted.web import resource as web_resource from twisted.web import util as web_util from twisted.web import vhost -from . import proxy -from twisted.python.components import registerAdapter -from twisted.python import failure -from twisted.python import filepath from twisted.words.protocols.jabber import jid -from sat.core.log import getLogger - -from sat_frontends.bridge.dbus_bridge import ( - Bridge, - BridgeExceptionNoService, - const_TIMEOUT as BRIDGE_TIMEOUT, -) -from sat.core.i18n import _, D_ -from sat.core import exceptions -from sat.tools import utils -from sat.tools import config -from sat.tools.common import regex -from sat.tools.common import template -from sat.tools.common import uri as common_uri -from sat.tools.common.utils import recursive_update, OrderedSet -from sat.tools.common import data_format -from sat.tools.common import tls -from sat_frontends.bridge.bridge_frontend import BridgeException import libervia from libervia.server import websockets +from libervia.server import session_iface +from libervia.server.constants import Const as C from libervia.server.pages import LiberviaPage -from libervia.server.utils import quote, ProgressHandler from libervia.server.tasks.manager import TasksManager -from functools import partial +from libervia.server.utils import ProgressHandler, quote -from libervia.server.constants import Const as C -from libervia.server import session_iface +from . import proxy from .restricted_bridge import RestrictedBridge log = getLogger(__name__) @@ -289,20 +289,44 @@ resource ) - async def _startApp(self, app_name, extra=None): + async def _start_app(self, app_name, extra=None) -> dict: + """Start a Libervia App + + @param app_name: canonical application name + @param extra: extra parameter to configure app + @return: app data + app data will not include computed exposed data, at this needs to wait for the + app to be started + """ if extra is None: extra = {} log.info(_( "starting application {app_name}").format(app_name=app_name)) - await self.host.bridgeCall( - "applicationStart", app_name, data_format.serialise(extra) + app_data = data_format.deserialise( + await self.host.bridgeCall( + "applicationStart", app_name, data_format.serialise(extra) + ) ) - app_data = self.libervia_apps[app_name] = data_format.deserialise( - await self.host.bridgeCall( - "applicationExposedGet", app_name, "", "")) + if app_data.get("started", False): + log.debug(f"application {app_name!r} is already started or starting") + # we do not await on purpose, the workflow should not be blocking at this + # point + defer.ensureDeferred(self._on_app_started(app_name, app_data["instance"])) + else: + self.host.apps_cb[app_data["instance"]] = self._on_app_started + return app_data + + async def _on_app_started( + self, + app_name: str, + instance_id: str + ) -> None: + exposed_data = self.libervia_apps[app_name] = data_format.deserialise( + await self.host.bridgeCall("applicationExposedGet", app_name, "", "") + ) try: - web_port = int(app_data['ports']['web'].split(':')[1]) + web_port = int(exposed_data['ports']['web'].split(':')[1]) except (KeyError, ValueError): log.warning(_( "no web port found for application {app_name!r}, can't use it " @@ -310,7 +334,7 @@ raise exceptions.DataError("no web port found") try: - url_prefix = app_data['url_prefix'].strip().rstrip('/') + url_prefix = exposed_data['url_prefix'].strip().rstrip('/') except (KeyError, AttributeError) as e: log.warning(_( "no URL prefix specified for this application, we can't embed it: {msg}") @@ -327,8 +351,9 @@ url_prefix.encode() ) self.addResourceToPath(url_prefix, res) - - return app_data + log.info( + f"Resource for app {app_name!r} (instance {instance_id!r}) has been added" + ) async def _initRedirections(self, options): url_redirections = options["url_redirections_dict"] @@ -502,19 +527,17 @@ app_name = urllib.parse.unquote(new_url.path).lower().strip() extra = {"url_prefix": f"/{old}"} try: - await self._startApp(app_name, extra) + await self._start_app(app_name, extra) except Exception as e: log.warning(_( "Can't launch {app_name!r} for path /{old}: {e}").format( app_name=app_name, old=old, e=e)) continue - log.info("[{host_name}] Added redirection from /{old} to application " - "{app_name}".format( - host_name=self.host_name, - old=old, - app_name=app_name)) - + log.info( + f"[{self.host_name}] Added redirection from /{old} to " + f"application {app_name}" + ) # normal redirection system is not used here continue elif new_url.scheme == "proxy": @@ -574,11 +597,13 @@ page_name, url = menu elif menu.startswith("libervia-app:"): app_name = menu[13:].strip().lower() - app_data = await self._startApp(app_name) - front_url = app_data['front_url'] + app_data = await self._start_app(app_name) + exposed_data = app_data["expose"] + front_url = exposed_data['front_url'] options = self.host.options url_redirections = options["url_redirections_dict"].setdefault( - self.site_name, {}) + self.site_name, {} + ) if front_url in url_redirections: raise exceptions.ConflictError( f"There is already a redirection from {front_url!r}, can't add " @@ -589,7 +614,7 @@ "path_args": [app_name] } - page_name = app_data.get('web_label', app_name).title() + page_name = exposed_data.get('web_label', app_name).title() url = front_url log.debug( @@ -879,6 +904,10 @@ self.bridge = Bridge() self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) + ## libervia app callbacks ## + # mapping instance id to the callback to call on "started" signal + self.apps_cb: Dict[str, Callable] = {} + @property def roots(self): """Return available virtual host roots @@ -1214,6 +1243,14 @@ "messageNew", partial(self.on_signal, "messageNew") ) + # libervia applications handling + self.bridge.register_signal( + "application_started", self.application_started_handler, "plugin" + ) + self.bridge.register_signal( + "application_error", self.application_error_handler, "plugin" + ) + #  Progress handling self.bridge.register_signal( "progressStarted", partial(ProgressHandler._signal, "started") @@ -1339,6 +1376,29 @@ for socket in sockets: socket.send("bridge", {"signal": signal_name, "args": args}) + def application_started_handler( + self, + name: str, + instance_id: str, + extra_s: str + ) -> None: + callback = self.apps_cb.pop(instance_id, None) + if callback is not None: + defer.ensureDeferred(callback(str(name), str(instance_id))) + + def application_error_handler( + self, + name: str, + instance_id: str, + extra_s: str + ) -> None: + callback = self.apps_cb.pop(instance_id, None) + if callback is not None: + extra = data_format.deserialise(extra_s) + log.error( + f"Can't start application {name}: {extra['class']}\n{extra['msg']}" + ) + async def _logged(self, profile, request): """Set everything when a user just logged in