Mercurial > libervia-web
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 |