Skip to content

Commit af4b9f7

Browse files
danielhbalinefm
authored andcommitted
Asynchronous UI notification implementation
This patch makes backend and UI changes to implement the asynchronous UI notification in WoK. - Backend: A push server was implemented from scratch to manage the opened websocket connections. The push server connects to the /run/user/<user_id>/woknotifications UNIX socket and broadcasts all messages to all connections. The websocket module is the same module that exists in the Kimchi plug-in. The idea is to remove the module from Kimchi and make it use the module from WoK. ws_proxy initialization was also added in src/wok/server.py. A change were made in Wok base control classes to allow every call of log_request to also send a websocket notification in the following format: <METHOD>:/<plugin>/<entity>/<action> For example, creating a new user in Ginger would trigger the following websocket notification: 'POST:/ginger/users' - Frontend: In ui/js/wok.main.js two new functions were added to help the usage of asynchronous notifications in the frontend. The idea: a single websocket is opened per session. This opened websocket will broadcast all incoming messages to all listeners registered. Listeners can be added by the new wok.addNotificationListener() method. This method will clean up any registered listener by itself when the user changes tabs/URL. The single websocket sends heartbeats to the backend side each 30 seconds. No reply from the backend is issued or expected. This heartbeat is just a way to ensure that the browser does not close the connection due to inactivity. This behavior varies from browser to browser but this 30 second heartbeat is more than enough to ensure that the websocket is kept alive. - Working example in User Log: A simple usage is provided in this patch. Changes were made in the UI of the User Log feature to refresh the listing each time a new log entry websocket notification is received. The idea is to allow this code to be a working example of how other tabs can consume the asynchronous notifications. Signed-off-by: Daniel Henrique Barboza <[email protected]>
1 parent fa21371 commit af4b9f7

16 files changed

+395
-14
lines changed

contrib/DEBIAN/control.in

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Depends: python-cherrypy3 (>= 3.2.0),
1616
fonts-font-awesome,
1717
logrotate,
1818
openssl,
19+
websockify,
1920
texlive-fonts-extra
2021
Build-Depends: xsltproc,
2122
gettext,

contrib/wok.spec.fedora.in

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Requires: fontawesome-fonts
2121
Requires: open-sans-fonts
2222
Requires: logrotate
2323
Requires: openssl
24+
Requires: python-websockify
2425
BuildRequires: gettext-devel
2526
BuildRequires: libxslt
2627
BuildRequires: python-lxml

contrib/wok.spec.suse.in

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Requires: fontawesome-fonts
2222
Requires: google-opensans-fonts
2323
Requires: logrotate
2424
Requires: openssl
25+
Requires: python-websockify
2526
BuildRequires: gettext-tools
2627
BuildRequires: libxslt-tools
2728
BuildRequires: python-lxml

docs/fedora-deps.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Runtime Dependencies
2828
$ sudo yum install python-cherrypy python-cheetah PyPAM m2crypto \
2929
python-jsonschema python-psutil python-ldap \
3030
python-lxml nginx openssl open-sans-fonts \
31-
fontawesome-fonts logrotate
31+
fontawesome-fonts logrotate python-websockify
3232

3333
# For RHEL systems, install the additional packages:
3434
$ sudo yum install python-ordereddict

docs/opensuse-deps.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Runtime Dependencies
1818
$ sudo zypper install python-CherryPy python-Cheetah python-pam \
1919
python-M2Crypto python-jsonschema python-psutil \
2020
python-ldap python-lxml python-xml nginx openssl \
21-
google-opensans-fonts fontawesome-fonts logrotate
21+
google-opensans-fonts fontawesome-fonts logrotate \
22+
python-websockify
2223

2324
Packages required for UI development
2425
------------------------------------

docs/ubuntu-deps.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Runtime Dependencies
1818
$ sudo apt-get install python-cherrypy3 python-cheetah python-pam \
1919
python-m2crypto python-jsonschema \
2020
python-psutil python-ldap python-lxml nginx \
21-
openssl fonts-font-awesome texlive-fonts-extra
21+
openssl fonts-font-awesome texlive-fonts-extra \
22+
websockify
2223

2324
Packages required for UI development
2425
------------------------------------

src/wok/config.py.in

+8
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,18 @@ def get_object_store():
6666
return os.path.join(paths.state_dir, 'objectstore')
6767

6868

69+
def get_pushserver_socket_dir():
70+
return '/run/user/%s' % os.geteuid()
71+
72+
6973
def get_version():
7074
return "-".join([__version__, __release__])
7175

7276

77+
def get_wstokens_dir():
78+
return os.path.join(paths.state_dir, 'ws-tokens')
79+
80+
7381
class Paths(object):
7482

7583
def __init__(self):

src/wok/control/base.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ def wrapper(*args, **kwargs):
163163
# log request
164164
code = self.getRequestMessage(method, action_name)
165165
reqParams = utf8_dict(self.log_args, request)
166-
log_id = log_request(code, reqParams, details, method, status)
166+
log_id = log_request(code, reqParams, details, method, status,
167+
class_name=get_class_name(self),
168+
action_name=action_name)
167169
if status == 202:
168170
save_request_log_id(log_id, action_result['id'])
169171

@@ -218,7 +220,8 @@ def index(self, *args, **kargs):
218220
# log request
219221
if method not in LOG_DISABLED_METHODS and status != 202:
220222
code = self.getRequestMessage(method)
221-
log_request(code, self.log_args, details, method, status)
223+
log_request(code, self.log_args, details, method, status,
224+
class_name=get_class_name(self))
222225

223226
return result
224227

@@ -306,7 +309,8 @@ def delete(self):
306309
code = self.getRequestMessage(method)
307310
reqParams = utf8_dict(self.log_args)
308311
log_id = log_request(code, reqParams, None, method,
309-
cherrypy.response.status)
312+
cherrypy.response.status,
313+
class_name=get_class_name(self))
310314
save_request_log_id(log_id, task['id'])
311315

312316
return wok.template.render("Task", task)
@@ -458,7 +462,8 @@ def index(self, *args, **kwargs):
458462
# log request
459463
code = self.getRequestMessage(method)
460464
reqParams = utf8_dict(self.log_args, params)
461-
log_request(code, reqParams, details, method, status)
465+
log_request(code, reqParams, details, method, status,
466+
class_name=get_class_name(self))
462467

463468

464469
class AsyncCollection(Collection):
@@ -486,7 +491,8 @@ def create(self, params, *args):
486491
code = self.getRequestMessage(method)
487492
reqParams = utf8_dict(self.log_args, params)
488493
log_id = log_request(code, reqParams, None, method,
489-
cherrypy.response.status)
494+
cherrypy.response.status,
495+
class_name=get_class_name(self))
490496
save_request_log_id(log_id, task['id'])
491497

492498
return wok.template.render("Task", task)

src/wok/model/notifications.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# Project Wok
33
#
4-
# Copyright IBM Corp, 2016
4+
# Copyright IBM Corp, 2016-2017
55
#
66
# This library is free software; you can redistribute it and/or
77
# modify it under the terms of the GNU Lesser General Public

src/wok/pushserver.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#
2+
# Project Wok
3+
#
4+
# Copyright IBM Corp, 2017
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+
#
20+
21+
import cherrypy
22+
import os
23+
import select
24+
import socket
25+
import threading
26+
27+
import wok.websocket as websocket
28+
from wok.config import get_pushserver_socket_dir
29+
from wok.utils import wok_log
30+
31+
32+
BASE_DIRECTORY = get_pushserver_socket_dir()
33+
TOKEN_NAME = 'woknotifications'
34+
END_OF_MESSAGE_MARKER = '//EOM//'
35+
push_server = None
36+
37+
38+
def start_push_server():
39+
global push_server
40+
41+
if not push_server:
42+
push_server = PushServer()
43+
44+
45+
def send_websocket_notification(message):
46+
global push_server
47+
48+
if push_server:
49+
push_server.send_notification(message)
50+
51+
52+
def send_wok_notification(uri, entity, method, action_name=None):
53+
app_name = 'wok'
54+
app = cherrypy.tree.apps.get(uri)
55+
if app:
56+
app_name = app.root.domain
57+
58+
source = '/%s/%s' % (app_name, entity)
59+
if action_name:
60+
source = '%s/%s' % (source, action_name)
61+
message = '%s:%s' % (method, source)
62+
send_websocket_notification(message)
63+
64+
65+
class PushServer(object):
66+
67+
def set_socket_file(self):
68+
if not os.path.isdir(BASE_DIRECTORY):
69+
try:
70+
os.mkdir(BASE_DIRECTORY)
71+
except OSError:
72+
raise RuntimeError('PushServer base UNIX socket dir %s '
73+
'not found.' % BASE_DIRECTORY)
74+
75+
self.server_addr = os.path.join(BASE_DIRECTORY, TOKEN_NAME)
76+
77+
if os.path.exists(self.server_addr):
78+
try:
79+
os.remove(self.server_addr)
80+
except:
81+
raise RuntimeError('There is an existing connection in %s' %
82+
self.server_addr)
83+
84+
def __init__(self):
85+
self.set_socket_file()
86+
87+
websocket.add_proxy_token(TOKEN_NAME, self.server_addr, True)
88+
89+
self.connections = []
90+
91+
self.server_running = True
92+
self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
93+
self.server_socket.setsockopt(socket.SOL_SOCKET,
94+
socket.SO_REUSEADDR, 1)
95+
self.server_socket.bind(self.server_addr)
96+
self.server_socket.listen(10)
97+
wok_log.info('Push server created on address %s' % self.server_addr)
98+
99+
self.connections.append(self.server_socket)
100+
cherrypy.engine.subscribe('stop', self.close_server, 1)
101+
102+
server_loop = threading.Thread(target=self.listen)
103+
server_loop.setDaemon(True)
104+
server_loop.start()
105+
106+
def listen(self):
107+
try:
108+
while self.server_running:
109+
read_ready, _, _ = select.select(self.connections,
110+
[], [], 1)
111+
for sock in read_ready:
112+
if not self.server_running:
113+
break
114+
115+
if sock == self.server_socket:
116+
117+
new_socket, addr = self.server_socket.accept()
118+
self.connections.append(new_socket)
119+
else:
120+
try:
121+
data = sock.recv(4096)
122+
except:
123+
try:
124+
self.connections.remove(sock)
125+
except ValueError:
126+
pass
127+
128+
continue
129+
if data and data == 'CLOSE':
130+
sock.send('ACK')
131+
try:
132+
self.connections.remove(sock)
133+
except ValueError:
134+
pass
135+
sock.close()
136+
137+
except Exception as e:
138+
raise RuntimeError('Exception ocurred in listen() of pushserver '
139+
'module: %s' % e.message)
140+
141+
def send_notification(self, message):
142+
message += END_OF_MESSAGE_MARKER
143+
for sock in self.connections:
144+
if sock != self.server_socket:
145+
try:
146+
sock.send(message)
147+
except IOError as e:
148+
if 'Broken pipe' in str(e):
149+
sock.close()
150+
try:
151+
self.connections.remove(sock)
152+
except ValueError:
153+
pass
154+
155+
def close_server(self):
156+
try:
157+
self.server_running = False
158+
self.server_socket.shutdown(socket.SHUT_RDWR)
159+
self.server_socket.close()
160+
os.remove(self.server_addr)
161+
except:
162+
pass
163+
finally:
164+
cherrypy.engine.unsubscribe('stop', self.close_server)

src/wok/reqlogger.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# Project Wok
33
#
4-
# Copyright IBM Corp, 2016
4+
# Copyright IBM Corp, 2016-2017
55
#
66
# This library is free software; you can redistribute it and/or
77
# modify it under the terms of the GNU Lesser General Public
@@ -34,6 +34,7 @@
3434
from wok.config import get_log_download_path, paths
3535
from wok.exception import InvalidParameter, OperationFailed
3636
from wok.message import WokMessage
37+
from wok.pushserver import send_wok_notification
3738
from wok.stringutils import ascii_dict
3839
from wok.utils import remove_old_files
3940

@@ -68,9 +69,11 @@
6869
# AsyncTask handling
6970
ASYNCTASK_REQUEST_METHOD = 'TASK'
7071

72+
NEW_LOG_ENTRY_MESSAGE = 'new_log_entry'
73+
7174

7275
def log_request(code, params, exception, method, status, app=None, user=None,
73-
ip=None):
76+
ip=None, class_name=None, action_name=None):
7477
'''
7578
Add an entry to user request log
7679
@@ -114,6 +117,10 @@ def log_request(code, params, exception, method, status, app=None, user=None,
114117
ip=ip
115118
).log()
116119

120+
if class_name:
121+
send_wok_notification(app, class_name, method, action_name)
122+
send_wok_notification('', 'logs', 'POST')
123+
117124
return log_id
118125

119126

src/wok/root.py

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from wok.control.base import Resource
3535
from wok.control.utils import parse_request, validate_params
3636
from wok.exception import OperationFailed, UnauthorizedError, WokException
37+
from wok.pushserver import send_wok_notification
3738
from wok.reqlogger import log_request
3839

3940

@@ -235,6 +236,7 @@ def _raise_timeout(user_id):
235236
status = e.getHttpStatusCode()
236237
raise cherrypy.HTTPError(401, e.message)
237238
finally:
239+
send_wok_notification('', 'login', 'POST')
238240
log_request(code, params, details, method, status)
239241

240242
return json.dumps(user_info)
@@ -247,6 +249,7 @@ def logout(self):
247249

248250
auth.logout()
249251

252+
send_wok_notification('', 'logout', 'POST')
250253
log_request(code, params, None, method, 200, user=params['username'])
251254

252255
return '{}'

0 commit comments

Comments
 (0)