comparison libervia/server/server.py @ 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
comparison
equal deleted inserted replaced
1504:409d10211b20 1505:a169cbc315f0
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from functools import partial
20 import os.path
21 from pathlib import Path
19 import re 22 import re
20 import os.path
21 import sys 23 import sys
24 import time
25 from typing import Dict, Optional, Callable
26 import urllib.error
22 import urllib.parse 27 import urllib.parse
23 import urllib.request, urllib.error 28 import urllib.request
24 import time 29
25 import copy 30 from sat.core import exceptions
26 from typing import Optional 31 from sat.core.i18n import D_, _
27 from pathlib import Path 32 from sat.core.log import getLogger
33 from sat.tools import utils
34 from sat.tools import config
35 from sat.tools.common import regex
36 from sat.tools.common import template
37 from sat.tools.common import uri as common_uri
38 from sat.tools.common import data_format
39 from sat.tools.common import tls
40 from sat.tools.common.utils import OrderedSet, recursive_update
41 from sat_frontends.bridge.bridge_frontend import BridgeException
42 from sat_frontends.bridge.dbus_bridge import (
43 Bridge,
44 BridgeExceptionNoService,
45 const_TIMEOUT as BRIDGE_TIMEOUT,
46 )
28 from twisted.application import service 47 from twisted.application import service
29 from twisted.internet import reactor, defer, inotify 48 from twisted.internet import defer, inotify, reactor
49 from twisted.python import failure
50 from twisted.python import filepath
51 from twisted.python.components import registerAdapter
30 from twisted.web import server 52 from twisted.web import server
31 from twisted.web import static 53 from twisted.web import static
32 from twisted.web import resource as web_resource 54 from twisted.web import resource as web_resource
33 from twisted.web import util as web_util 55 from twisted.web import util as web_util
34 from twisted.web import vhost 56 from twisted.web import vhost
35 from . import proxy
36 from twisted.python.components import registerAdapter
37 from twisted.python import failure
38 from twisted.python import filepath
39 from twisted.words.protocols.jabber import jid 57 from twisted.words.protocols.jabber import jid
40 58
41 from sat.core.log import getLogger
42
43 from sat_frontends.bridge.dbus_bridge import (
44 Bridge,
45 BridgeExceptionNoService,
46 const_TIMEOUT as BRIDGE_TIMEOUT,
47 )
48 from sat.core.i18n import _, D_
49 from sat.core import exceptions
50 from sat.tools import utils
51 from sat.tools import config
52 from sat.tools.common import regex
53 from sat.tools.common import template
54 from sat.tools.common import uri as common_uri
55 from sat.tools.common.utils import recursive_update, OrderedSet
56 from sat.tools.common import data_format
57 from sat.tools.common import tls
58 from sat_frontends.bridge.bridge_frontend import BridgeException
59 import libervia 59 import libervia
60 from libervia.server import websockets 60 from libervia.server import websockets
61 from libervia.server import session_iface
62 from libervia.server.constants import Const as C
61 from libervia.server.pages import LiberviaPage 63 from libervia.server.pages import LiberviaPage
62 from libervia.server.utils import quote, ProgressHandler
63 from libervia.server.tasks.manager import TasksManager 64 from libervia.server.tasks.manager import TasksManager
64 from functools import partial 65 from libervia.server.utils import ProgressHandler, quote
65 66
66 from libervia.server.constants import Const as C 67 from . import proxy
67 from libervia.server import session_iface
68 from .restricted_bridge import RestrictedBridge 68 from .restricted_bridge import RestrictedBridge
69 69
70 log = getLogger(__name__) 70 log = getLogger(__name__)
71 71
72 72
287 current.putChild( 287 current.putChild(
288 last_segment.encode('utf-8'), 288 last_segment.encode('utf-8'),
289 resource 289 resource
290 ) 290 )
291 291
292 async def _startApp(self, app_name, extra=None): 292 async def _start_app(self, app_name, extra=None) -> dict:
293 """Start a Libervia App
294
295 @param app_name: canonical application name
296 @param extra: extra parameter to configure app
297 @return: app data
298 app data will not include computed exposed data, at this needs to wait for the
299 app to be started
300 """
293 if extra is None: 301 if extra is None:
294 extra = {} 302 extra = {}
295 log.info(_( 303 log.info(_(
296 "starting application {app_name}").format(app_name=app_name)) 304 "starting application {app_name}").format(app_name=app_name))
297 await self.host.bridgeCall( 305 app_data = data_format.deserialise(
298 "applicationStart", app_name, data_format.serialise(extra)
299 )
300 app_data = self.libervia_apps[app_name] = data_format.deserialise(
301 await self.host.bridgeCall( 306 await self.host.bridgeCall(
302 "applicationExposedGet", app_name, "", "")) 307 "applicationStart", app_name, data_format.serialise(extra)
308 )
309 )
310 if app_data.get("started", False):
311 log.debug(f"application {app_name!r} is already started or starting")
312 # we do not await on purpose, the workflow should not be blocking at this
313 # point
314 defer.ensureDeferred(self._on_app_started(app_name, app_data["instance"]))
315 else:
316 self.host.apps_cb[app_data["instance"]] = self._on_app_started
317 return app_data
318
319 async def _on_app_started(
320 self,
321 app_name: str,
322 instance_id: str
323 ) -> None:
324 exposed_data = self.libervia_apps[app_name] = data_format.deserialise(
325 await self.host.bridgeCall("applicationExposedGet", app_name, "", "")
326 )
303 327
304 try: 328 try:
305 web_port = int(app_data['ports']['web'].split(':')[1]) 329 web_port = int(exposed_data['ports']['web'].split(':')[1])
306 except (KeyError, ValueError): 330 except (KeyError, ValueError):
307 log.warning(_( 331 log.warning(_(
308 "no web port found for application {app_name!r}, can't use it " 332 "no web port found for application {app_name!r}, can't use it "
309 ).format(app_name=app_name)) 333 ).format(app_name=app_name))
310 raise exceptions.DataError("no web port found") 334 raise exceptions.DataError("no web port found")
311 335
312 try: 336 try:
313 url_prefix = app_data['url_prefix'].strip().rstrip('/') 337 url_prefix = exposed_data['url_prefix'].strip().rstrip('/')
314 except (KeyError, AttributeError) as e: 338 except (KeyError, AttributeError) as e:
315 log.warning(_( 339 log.warning(_(
316 "no URL prefix specified for this application, we can't embed it: {msg}") 340 "no URL prefix specified for this application, we can't embed it: {msg}")
317 .format(msg=e)) 341 .format(msg=e))
318 raise exceptions.DataError("no URL prefix") 342 raise exceptions.DataError("no URL prefix")
325 "localhost", 349 "localhost",
326 web_port, 350 web_port,
327 url_prefix.encode() 351 url_prefix.encode()
328 ) 352 )
329 self.addResourceToPath(url_prefix, res) 353 self.addResourceToPath(url_prefix, res)
330 354 log.info(
331 return app_data 355 f"Resource for app {app_name!r} (instance {instance_id!r}) has been added"
356 )
332 357
333 async def _initRedirections(self, options): 358 async def _initRedirections(self, options):
334 url_redirections = options["url_redirections_dict"] 359 url_redirections = options["url_redirections_dict"]
335 360
336 url_redirections = url_redirections.get(self.site_name, {}) 361 url_redirections = url_redirections.get(self.site_name, {})
500 # a Libervia application 525 # a Libervia application
501 526
502 app_name = urllib.parse.unquote(new_url.path).lower().strip() 527 app_name = urllib.parse.unquote(new_url.path).lower().strip()
503 extra = {"url_prefix": f"/{old}"} 528 extra = {"url_prefix": f"/{old}"}
504 try: 529 try:
505 await self._startApp(app_name, extra) 530 await self._start_app(app_name, extra)
506 except Exception as e: 531 except Exception as e:
507 log.warning(_( 532 log.warning(_(
508 "Can't launch {app_name!r} for path /{old}: {e}").format( 533 "Can't launch {app_name!r} for path /{old}: {e}").format(
509 app_name=app_name, old=old, e=e)) 534 app_name=app_name, old=old, e=e))
510 continue 535 continue
511 536
512 log.info("[{host_name}] Added redirection from /{old} to application " 537 log.info(
513 "{app_name}".format( 538 f"[{self.host_name}] Added redirection from /{old} to "
514 host_name=self.host_name, 539 f"application {app_name}"
515 old=old, 540 )
516 app_name=app_name))
517
518 # normal redirection system is not used here 541 # normal redirection system is not used here
519 continue 542 continue
520 elif new_url.scheme == "proxy": 543 elif new_url.scheme == "proxy":
521 # a reverse proxy 544 # a reverse proxy
522 host, port = new_url.hostname, new_url.port 545 host, port = new_url.hostname, new_url.port
572 log.error(msg) 595 log.error(msg)
573 raise ValueError(msg) 596 raise ValueError(msg)
574 page_name, url = menu 597 page_name, url = menu
575 elif menu.startswith("libervia-app:"): 598 elif menu.startswith("libervia-app:"):
576 app_name = menu[13:].strip().lower() 599 app_name = menu[13:].strip().lower()
577 app_data = await self._startApp(app_name) 600 app_data = await self._start_app(app_name)
578 front_url = app_data['front_url'] 601 exposed_data = app_data["expose"]
602 front_url = exposed_data['front_url']
579 options = self.host.options 603 options = self.host.options
580 url_redirections = options["url_redirections_dict"].setdefault( 604 url_redirections = options["url_redirections_dict"].setdefault(
581 self.site_name, {}) 605 self.site_name, {}
606 )
582 if front_url in url_redirections: 607 if front_url in url_redirections:
583 raise exceptions.ConflictError( 608 raise exceptions.ConflictError(
584 f"There is already a redirection from {front_url!r}, can't add " 609 f"There is already a redirection from {front_url!r}, can't add "
585 f"{app_name!r}") 610 f"{app_name!r}")
586 611
587 url_redirections[front_url] = { 612 url_redirections[front_url] = {
588 "page": 'embed_app', 613 "page": 'embed_app',
589 "path_args": [app_name] 614 "path_args": [app_name]
590 } 615 }
591 616
592 page_name = app_data.get('web_label', app_name).title() 617 page_name = exposed_data.get('web_label', app_name).title()
593 url = front_url 618 url = front_url
594 619
595 log.debug( 620 log.debug(
596 f"Application {app_name} added to menu of {self.site_name}" 621 f"Application {app_name} added to menu of {self.site_name}"
597 ) 622 )
876 901
877 ## bridge ## 902 ## bridge ##
878 self._bridge_retry = self.options['bridge-retries'] 903 self._bridge_retry = self.options['bridge-retries']
879 self.bridge = Bridge() 904 self.bridge = Bridge()
880 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) 905 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
906
907 ## libervia app callbacks ##
908 # mapping instance id to the callback to call on "started" signal
909 self.apps_cb: Dict[str, Callable] = {}
881 910
882 @property 911 @property
883 def roots(self): 912 def roots(self):
884 """Return available virtual host roots 913 """Return available virtual host roots
885 914
1212 ) 1241 )
1213 self.bridge.register_signal( 1242 self.bridge.register_signal(
1214 "messageNew", partial(self.on_signal, "messageNew") 1243 "messageNew", partial(self.on_signal, "messageNew")
1215 ) 1244 )
1216 1245
1246 # libervia applications handling
1247 self.bridge.register_signal(
1248 "application_started", self.application_started_handler, "plugin"
1249 )
1250 self.bridge.register_signal(
1251 "application_error", self.application_error_handler, "plugin"
1252 )
1253
1217 #  Progress handling 1254 #  Progress handling
1218 self.bridge.register_signal( 1255 self.bridge.register_signal(
1219 "progressStarted", partial(ProgressHandler._signal, "started") 1256 "progressStarted", partial(ProgressHandler._signal, "started")
1220 ) 1257 )
1221 self.bridge.register_signal( 1258 self.bridge.register_signal(
1336 except KeyError: 1373 except KeyError:
1337 log.debug(f"no socket opened for profile {profile}") 1374 log.debug(f"no socket opened for profile {profile}")
1338 return 1375 return
1339 for socket in sockets: 1376 for socket in sockets:
1340 socket.send("bridge", {"signal": signal_name, "args": args}) 1377 socket.send("bridge", {"signal": signal_name, "args": args})
1378
1379 def application_started_handler(
1380 self,
1381 name: str,
1382 instance_id: str,
1383 extra_s: str
1384 ) -> None:
1385 callback = self.apps_cb.pop(instance_id, None)
1386 if callback is not None:
1387 defer.ensureDeferred(callback(str(name), str(instance_id)))
1388
1389 def application_error_handler(
1390 self,
1391 name: str,
1392 instance_id: str,
1393 extra_s: str
1394 ) -> None:
1395 callback = self.apps_cb.pop(instance_id, None)
1396 if callback is not None:
1397 extra = data_format.deserialise(extra_s)
1398 log.error(
1399 f"Can't start application {name}: {extra['class']}\n{extra['msg']}"
1400 )
1341 1401
1342 async def _logged(self, profile, request): 1402 async def _logged(self, profile, request):
1343 """Set everything when a user just logged in 1403 """Set everything when a user just logged in
1344 1404
1345 @param profile 1405 @param profile