comparison src/server/websockets.py @ 995:f88325b56a6a

server: dynamic pages first draft: /!\ new dependency: autobahn This patch introduce server part of dynamic pages. Dynamic pages use websockets to establish constant connection with a Libervia page, allowing to receive real time data or update it. The feature is activated by specifying "dynamic = true" in the page. Once activated, page can implement "on_data" method which will be called when data are sent by the page. To send data the other way, the page can use request.sendData. The new "registerSignal" method allows to use an "on_signal" method to be called each time given signal is received, with automatic (and optional) filtering on profile. New renderPartial and renderAndUpdate method allow to append new HTML elements to the dynamic page.
author Goffi <goffi@goffi.org>
date Wed, 03 Jan 2018 01:10:12 +0100
parents
children f2170536ba23
comparison
equal deleted inserted replaced
994:b92b06f023cb 995:f88325b56a6a
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011-2017 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.log import getLogger
22 log = getLogger(__name__)
23 from sat.core import exceptions
24
25 from autobahn.twisted import websocket
26 from autobahn.twisted import resource as resource
27 from autobahn.websocket import types
28
29 import json
30
31 LIBERVIA_PROTOCOL = 'libervia_page'
32
33
34 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol):
35 host = None
36 tokens_map = {}
37
38 def onConnect(self, request):
39 prefix = LIBERVIA_PROTOCOL + u'_'
40 for protocol in request.protocols:
41 if protocol.startswith(prefix):
42 token = protocol[len(prefix):].strip()
43 if token:
44 break
45 else:
46 raise types.ConnectionDeny(types.ConnectionDeny.NOT_IMPLEMENTED,
47 u"Can't use this subprotocol")
48
49 if token not in self.tokens_map:
50 log.warning(_(u"Can't activate page socket: unknown token"))
51 raise types.ConnectionDeny(types.ConnectionDeny.FORBIDDEN,
52 u"Bad token, please reload page")
53 self.token = token
54 self.page = self.tokens_map[token]['page']
55 self.request = self.tokens_map[token]['request']
56 return protocol
57
58 def onOpen(self):
59 log.debug(_(u"Websocket opened for {page} (token: {token})".format(
60 page = self.page,
61 token = self.token)))
62 self.request.sendData = self.sendJSONData
63 self.page.onSocketOpen(self.request)
64
65 def onMessage(self, payload, isBinary):
66 try:
67 data_json = json.loads(payload.decode('utf8'))
68 except ValueError as e:
69 log.warning(_(u"Not valid JSON, ignoring data: {msg}\n{data}").format(msg=e, data=payload))
70 return
71 # we request page first, to raise an AttributeError
72 # if it is not set (which should never happen)
73 page = self.page
74 try:
75 cb = page.on_data
76 except AttributeError:
77 log.warning(_(u'No "on_data" method set on dynamic page, ignoring data:\n{data}').format(data=data_json))
78 else:
79 cb(page, self.request, data_json)
80
81 def onClose(self, wasClean, code, reason):
82 try:
83 token = self.token
84 except AttributeError:
85 log.warning(_(u'Websocket closed but no token is associated'))
86 return
87
88 self.page.onSocketClose(self.request)
89
90 try:
91 del self.tokens_map[token]
92 del self.request.sendData
93 except (KeyError, AttributeError):
94 raise exceptions.InternalError(_(u"Token or sendData doesn't exist, this should never happen!"))
95 log.debug(_(u"Websocket closed for {page} (token: {token}). {reason}".format(
96 page = self.page,
97 token = self.token,
98 reason = u'' if wasClean else _(u'Reason: {reason}').format(reason=reason))))
99
100 def sendJSONData(self, type_, **data):
101 assert 'type' not in data
102 data['type'] = type_
103 self.sendMessage(json.dumps(data, ensure_ascii = False).encode('utf8'))
104
105 @classmethod
106 def getBaseURL(cls, host, secure):
107 return u"ws{sec}://localhost:{port}".format(
108 sec='s' if secure else '',
109 port=cls.host.options['port_https' if secure else 'port'])
110
111 @classmethod
112 def getResource(cls, host, secure):
113 if cls.host is None:
114 cls.host = host
115 factory = websocket.WebSocketServerFactory(cls.getBaseURL(host, secure))
116 factory.protocol = cls
117 return resource.WebSocketResource(factory)
118
119 @classmethod
120 def registerToken(cls, token, page, request):
121 if token in cls.tokens_map:
122 raise exceptions.ConflictError(_(u'This token is already registered'))
123 cls.tokens_map[token] = {'page': page,
124 'request': request}