changeset 1505:a169cbc315f0

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.
author Goffi <goffi@goffi.org>
date Sat, 04 Mar 2023 18:37:17 +0100
parents 409d10211b20
children ce879da7fcf7
files libervia/server/server.py
diffstat 1 files changed, 114 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- 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 <http://www.gnu.org/licenses/>.
 
+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