[Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation

dhbarboza82@gmail.com posted 1 patch 7 years, 9 months ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/kimchi tags/patchew/20170307185246.21301-2-dhbarboza82@gmail.com
contrib/DEBIAN/control.in      |   1 +
contrib/wok.spec.fedora.in     |   1 +
contrib/wok.spec.suse.in       |   1 +
docs/fedora-deps.md            |   2 +-
docs/opensuse-deps.md          |   3 +-
docs/ubuntu-deps.md            |   3 +-
src/wok/config.py.in           |   8 ++
src/wok/control/base.py        |  16 ++--
src/wok/model/notifications.py |   2 +-
src/wok/pushserver.py          | 163 +++++++++++++++++++++++++++++++++++++++++
src/wok/reqlogger.py           |  11 ++-
src/wok/root.py                |   3 +
src/wok/server.py              |  13 +++-
src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
ui/js/src/wok.main.js          |  43 +++++++++++
ui/js/wok.user-log.js          |  12 +++
16 files changed, 391 insertions(+), 14 deletions(-)
create mode 100644 src/wok/pushserver.py
create mode 100644 src/wok/websocket.py
[Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation
Posted by dhbarboza82@gmail.com 7 years, 9 months ago
From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>

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 <danielhb@linux.vnet.ibm.com>
---
 contrib/DEBIAN/control.in      |   1 +
 contrib/wok.spec.fedora.in     |   1 +
 contrib/wok.spec.suse.in       |   1 +
 docs/fedora-deps.md            |   2 +-
 docs/opensuse-deps.md          |   3 +-
 docs/ubuntu-deps.md            |   3 +-
 src/wok/config.py.in           |   8 ++
 src/wok/control/base.py        |  16 ++--
 src/wok/model/notifications.py |   2 +-
 src/wok/pushserver.py          | 163 +++++++++++++++++++++++++++++++++++++++++
 src/wok/reqlogger.py           |  11 ++-
 src/wok/root.py                |   3 +
 src/wok/server.py              |  13 +++-
 src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
 ui/js/src/wok.main.js          |  43 +++++++++++
 ui/js/wok.user-log.js          |  12 +++
 16 files changed, 391 insertions(+), 14 deletions(-)
 create mode 100644 src/wok/pushserver.py
 create mode 100644 src/wok/websocket.py

diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in
index ba083b3..e825368 100644
--- a/contrib/DEBIAN/control.in
+++ b/contrib/DEBIAN/control.in
@@ -16,6 +16,7 @@ Depends: python-cherrypy3 (>= 3.2.0),
          fonts-font-awesome,
          logrotate,
          openssl,
+         websockify,
          texlive-fonts-extra
 Build-Depends: xsltproc,
                gettext,
diff --git a/contrib/wok.spec.fedora.in b/contrib/wok.spec.fedora.in
index 6af8222..7514376 100644
--- a/contrib/wok.spec.fedora.in
+++ b/contrib/wok.spec.fedora.in
@@ -21,6 +21,7 @@ Requires:	fontawesome-fonts
 Requires:	open-sans-fonts
 Requires:	logrotate
 Requires:	openssl
+Requires:	python-websockify
 BuildRequires:	gettext-devel
 BuildRequires:	libxslt
 BuildRequires:	python-lxml
diff --git a/contrib/wok.spec.suse.in b/contrib/wok.spec.suse.in
index db31616..cca3186 100644
--- a/contrib/wok.spec.suse.in
+++ b/contrib/wok.spec.suse.in
@@ -22,6 +22,7 @@ Requires:	fontawesome-fonts
 Requires:	google-opensans-fonts
 Requires:	logrotate
 Requires:	openssl
+Requires:	python-websockify
 BuildRequires:	gettext-tools
 BuildRequires:	libxslt-tools
 BuildRequires:	python-lxml
diff --git a/docs/fedora-deps.md b/docs/fedora-deps.md
index 260390e..29b477f 100644
--- a/docs/fedora-deps.md
+++ b/docs/fedora-deps.md
@@ -28,7 +28,7 @@ Runtime Dependencies
     $ sudo yum install python-cherrypy python-cheetah PyPAM m2crypto \
                         python-jsonschema python-psutil python-ldap \
                         python-lxml nginx openssl open-sans-fonts \
-                        fontawesome-fonts logrotate
+                        fontawesome-fonts logrotate python-websockify
 
     # For RHEL systems, install the additional packages:
     $ sudo yum install python-ordereddict
diff --git a/docs/opensuse-deps.md b/docs/opensuse-deps.md
index 7fe1763..a6086da 100644
--- a/docs/opensuse-deps.md
+++ b/docs/opensuse-deps.md
@@ -18,7 +18,8 @@ Runtime Dependencies
     $ sudo zypper install python-CherryPy python-Cheetah python-pam \
                           python-M2Crypto python-jsonschema python-psutil \
                           python-ldap python-lxml python-xml nginx openssl \
-                          google-opensans-fonts fontawesome-fonts logrotate
+                          google-opensans-fonts fontawesome-fonts logrotate \
+                          python-websockify
 
 Packages required for UI development
 ------------------------------------
diff --git a/docs/ubuntu-deps.md b/docs/ubuntu-deps.md
index 3a0f75c..cdffc59 100644
--- a/docs/ubuntu-deps.md
+++ b/docs/ubuntu-deps.md
@@ -18,7 +18,8 @@ Runtime Dependencies
     $ sudo apt-get install python-cherrypy3 python-cheetah python-pam \
                             python-m2crypto python-jsonschema \
                             python-psutil python-ldap python-lxml nginx \
-                            openssl fonts-font-awesome texlive-fonts-extra
+                            openssl fonts-font-awesome texlive-fonts-extra \
+                            websockify
 
 Packages required for UI development
 ------------------------------------
diff --git a/src/wok/config.py.in b/src/wok/config.py.in
index 97776dd..677d0ed 100644
--- a/src/wok/config.py.in
+++ b/src/wok/config.py.in
@@ -66,10 +66,18 @@ def get_object_store():
     return os.path.join(paths.state_dir, 'objectstore')
 
 
+def get_pushserver_socket_dir():
+    return '/run/user/%s' % os.geteuid()
+
+
 def get_version():
     return "-".join([__version__, __release__])
 
 
+def get_wstokens_dir():
+    return os.path.join(paths.state_dir, 'ws-tokens')
+
+
 class Paths(object):
 
     def __init__(self):
diff --git a/src/wok/control/base.py b/src/wok/control/base.py
index 0791062..b534b6f 100644
--- a/src/wok/control/base.py
+++ b/src/wok/control/base.py
@@ -163,7 +163,9 @@ class Resource(object):
                 # log request
                 code = self.getRequestMessage(method, action_name)
                 reqParams = utf8_dict(self.log_args, request)
-                log_id = log_request(code, reqParams, details, method, status)
+                log_id = log_request(code, reqParams, details, method, status,
+                                     class_name=get_class_name(self),
+                                     action_name=action_name)
                 if status == 202:
                     save_request_log_id(log_id, action_result['id'])
 
@@ -218,7 +220,8 @@ class Resource(object):
             # log request
             if method not in LOG_DISABLED_METHODS and status != 202:
                 code = self.getRequestMessage(method)
-                log_request(code, self.log_args, details, method, status)
+                log_request(code, self.log_args, details, method, status,
+                            class_name=get_class_name(self))
 
         return result
 
@@ -306,7 +309,8 @@ class AsyncResource(Resource):
         code = self.getRequestMessage(method)
         reqParams = utf8_dict(self.log_args)
         log_id = log_request(code, reqParams, None, method,
-                             cherrypy.response.status)
+                             cherrypy.response.status,
+                             class_name=get_class_name(self))
         save_request_log_id(log_id, task['id'])
 
         return wok.template.render("Task", task)
@@ -458,7 +462,8 @@ class Collection(object):
                 # log request
                 code = self.getRequestMessage(method)
                 reqParams = utf8_dict(self.log_args, params)
-                log_request(code, reqParams, details, method, status)
+                log_request(code, reqParams, details, method, status,
+                            class_name=get_class_name(self))
 
 
 class AsyncCollection(Collection):
@@ -486,7 +491,8 @@ class AsyncCollection(Collection):
         code = self.getRequestMessage(method)
         reqParams = utf8_dict(self.log_args, params)
         log_id = log_request(code, reqParams, None, method,
-                             cherrypy.response.status)
+                             cherrypy.response.status,
+                             class_name=get_class_name(self))
         save_request_log_id(log_id, task['id'])
 
         return wok.template.render("Task", task)
diff --git a/src/wok/model/notifications.py b/src/wok/model/notifications.py
index bdb7c78..f4eb542 100644
--- a/src/wok/model/notifications.py
+++ b/src/wok/model/notifications.py
@@ -1,7 +1,7 @@
 #
 # Project Wok
 #
-# Copyright IBM Corp, 2016
+# Copyright IBM Corp, 2016-2017
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
diff --git a/src/wok/pushserver.py b/src/wok/pushserver.py
new file mode 100644
index 0000000..4e89f36
--- /dev/null
+++ b/src/wok/pushserver.py
@@ -0,0 +1,163 @@
+#
+# Project Wok
+#
+# Copyright IBM Corp, 2017
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+#
+
+import cherrypy
+import os
+import select
+import socket
+import threading
+
+import wok.websocket as websocket
+from wok.config import get_pushserver_socket_dir
+from wok.utils import wok_log
+
+
+BASE_DIRECTORY = get_pushserver_socket_dir()
+TOKEN_NAME = 'woknotifications'
+END_OF_MESSAGE_MARKER = '//EOM//'
+push_server = None
+
+
+def start_push_server():
+    global push_server
+
+    if not push_server:
+        push_server = PushServer()
+
+
+def send_websocket_notification(message):
+    global push_server
+
+    if push_server:
+        push_server.send_notification(message)
+
+
+def send_wok_notification(uri, entity, method, action_name=None):
+    app_name = 'wok'
+    app = cherrypy.tree.apps.get(uri)
+    if app:
+        app_name = app.root.domain
+
+    source = '/%s/%s' % (app_name, entity)
+    if action_name:
+        source = '%s/%s' % (source, action_name)
+    message = '%s:%s' % (method, source)
+    send_websocket_notification(message)
+
+
+class PushServer(object):
+
+    def set_socket_file(self):
+        if not os.path.isdir(BASE_DIRECTORY):
+            try:
+                os.mkdir(BASE_DIRECTORY)
+            except OSError:
+                raise RuntimeError('PushServer base UNIX socket dir %s '
+                                   'not found.' % BASE_DIRECTORY)
+
+        self.server_addr = os.path.join(BASE_DIRECTORY, TOKEN_NAME)
+
+        if os.path.exists(self.server_addr):
+            try:
+                os.remove(self.server_addr)
+            except:
+                raise RuntimeError('There is an existing connection in %s' %
+                                   self.server_addr)
+
+    def __init__(self):
+        self.set_socket_file()
+
+        websocket.add_proxy_token(TOKEN_NAME, self.server_addr, True)
+
+        self.connections = []
+
+        self.server_running = True
+        self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.server_socket.setsockopt(socket.SOL_SOCKET,
+                                      socket.SO_REUSEADDR, 1)
+        self.server_socket.bind(self.server_addr)
+        self.server_socket.listen(10)
+        wok_log.info('Push server created on address %s' % self.server_addr)
+
+        self.connections.append(self.server_socket)
+        cherrypy.engine.subscribe('stop', self.close_server, 1)
+
+        server_loop = threading.Thread(target=self.listen)
+        server_loop.start()
+
+    def listen(self):
+        try:
+            while self.server_running:
+                read_ready, _, _ = select.select(self.connections,
+                                                 [], [], 1)
+                for sock in read_ready:
+                    if not self.server_running:
+                        break
+
+                    if sock == self.server_socket:
+
+                        new_socket, addr = self.server_socket.accept()
+                        self.connections.append(new_socket)
+                    else:
+                        try:
+                            data = sock.recv(4096)
+                        except:
+                            try:
+                                self.connections.remove(sock)
+                            except ValueError:
+                                pass
+
+                            continue
+                        if data and data == 'CLOSE':
+                            sock.send('ACK')
+                            try:
+                                self.connections.remove(sock)
+                            except ValueError:
+                                pass
+                            sock.close()
+
+        except Exception as e:
+            raise RuntimeError('Exception ocurred in listen() of pushserver '
+                               'module: %s' % e.message)
+
+    def send_notification(self, message):
+        message += END_OF_MESSAGE_MARKER
+        for sock in self.connections:
+            if sock != self.server_socket:
+                try:
+                    sock.send(message)
+                except IOError as e:
+                    if 'Broken pipe' in str(e):
+                        sock.close()
+                        try:
+                            self.connections.remove(sock)
+                        except ValueError:
+                            pass
+
+    def close_server(self):
+        try:
+            self.server_running = False
+            self.server_socket.shutdown(socket.SHUT_RDWR)
+            self.server_socket.close()
+            os.remove(self.server_addr)
+        except:
+            pass
+        finally:
+            cherrypy.engine.unsubscribe('stop', self.close_server)
diff --git a/src/wok/reqlogger.py b/src/wok/reqlogger.py
index 92e155d..02cd1d1 100644
--- a/src/wok/reqlogger.py
+++ b/src/wok/reqlogger.py
@@ -1,7 +1,7 @@
 #
 # Project Wok
 #
-# Copyright IBM Corp, 2016
+# Copyright IBM Corp, 2016-2017
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -34,6 +34,7 @@ from wok.auth import USER_NAME
 from wok.config import get_log_download_path, paths
 from wok.exception import InvalidParameter, OperationFailed
 from wok.message import WokMessage
+from wok.pushserver import send_wok_notification
 from wok.stringutils import ascii_dict
 from wok.utils import remove_old_files
 
@@ -68,9 +69,11 @@ WOK_REQUEST_LOGGER = "wok_request_logger"
 # AsyncTask handling
 ASYNCTASK_REQUEST_METHOD = 'TASK'
 
+NEW_LOG_ENTRY_MESSAGE = 'new_log_entry'
+
 
 def log_request(code, params, exception, method, status, app=None, user=None,
-                ip=None):
+                ip=None, class_name=None, action_name=None):
     '''
     Add an entry to user request log
 
@@ -114,6 +117,10 @@ def log_request(code, params, exception, method, status, app=None, user=None,
         ip=ip
     ).log()
 
+    if class_name:
+        send_wok_notification(app, class_name, method, action_name)
+    send_wok_notification('', 'logs', 'POST')
+
     return log_id
 
 
diff --git a/src/wok/root.py b/src/wok/root.py
index ea88c8c..cb7c24e 100644
--- a/src/wok/root.py
+++ b/src/wok/root.py
@@ -34,6 +34,7 @@ from wok.control import sub_nodes
 from wok.control.base import Resource
 from wok.control.utils import parse_request, validate_params
 from wok.exception import OperationFailed, UnauthorizedError, WokException
+from wok.pushserver import send_wok_notification
 from wok.reqlogger import log_request
 
 
@@ -235,6 +236,7 @@ class WokRoot(Root):
             status = e.getHttpStatusCode()
             raise cherrypy.HTTPError(401, e.message)
         finally:
+            send_wok_notification('', 'login', 'POST')
             log_request(code, params, details, method, status)
 
         return json.dumps(user_info)
@@ -247,6 +249,7 @@ class WokRoot(Root):
 
         auth.logout()
 
+        send_wok_notification('', 'logout', 'POST')
         log_request(code, params, None, method, 200, user=params['username'])
 
         return '{}'
diff --git a/src/wok/server.py b/src/wok/server.py
index b411012..1bd7b08 100644
--- a/src/wok/server.py
+++ b/src/wok/server.py
@@ -25,13 +25,13 @@ import logging
 import logging.handlers
 import os
 
-from wok import auth
-from wok import config
+from wok import auth, config, websocket
 from wok.config import config as configParser
 from wok.config import WokConfig
 from wok.control import sub_nodes
 from wok.model import model
 from wok.proxy import check_proxy_config
+from wok.pushserver import start_push_server
 from wok.reqlogger import RequestLogger
 from wok.root import WokRoot
 from wok.safewatchedfilehandler import SafeWatchedFileHandler
@@ -77,7 +77,8 @@ class Server(object):
             os.path.abspath(configParser.get("logging", "log_dir")),
             os.path.dirname(os.path.abspath(options.access_log)),
             os.path.dirname(os.path.abspath(options.error_log)),
-            os.path.dirname(os.path.abspath(config.get_object_store()))
+            os.path.dirname(os.path.abspath(config.get_object_store())),
+            os.path.abspath(config.get_wstokens_dir())
         ]
         for directory in make_dirs:
             if not os.path.isdir(directory):
@@ -160,9 +161,15 @@ class Server(object):
         cherrypy.tree.mount(WokRoot(model.Model(), dev_env),
                             options.server_root, self.configObj)
 
+        self._start_websocket_server()
         self._load_plugins()
         cherrypy.lib.sessions.init()
 
+    def _start_websocket_server(self):
+        start_push_server()
+        ws_proxy = websocket.new_ws_proxy()
+        cherrypy.engine.subscribe('exit', ws_proxy.terminate)
+
     def _load_plugins(self):
         for plugin_name, plugin_config in get_enabled_plugins():
             load_plugin(plugin_name, plugin_config)
diff --git a/src/wok/websocket.py b/src/wok/websocket.py
new file mode 100644
index 0000000..46d5409
--- /dev/null
+++ b/src/wok/websocket.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python2
+#
+# Project Wok
+#
+# Copyright IBM Corp, 2017
+#
+# Code derived from Project Kimchi
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+
+import base64
+import errno
+import os
+
+from multiprocessing import Process
+from websockify import WebSocketProxy
+
+from config import config, get_wstokens_dir
+
+
+try:
+    from websockify.token_plugins import TokenFile
+    tokenFile = True
+except ImportError:
+    tokenFile = False
+
+try:
+    from websockify import ProxyRequestHandler as request_proxy
+except:
+    from websockify import WebSocketProxy as request_proxy
+
+
+WS_TOKENS_DIR = get_wstokens_dir()
+
+
+class CustomHandler(request_proxy):
+
+    def get_target(self, target_plugin, path):
+        if issubclass(CustomHandler, object):
+            target = super(CustomHandler, self).get_target(target_plugin,
+                                                           path)
+        else:
+            target = request_proxy.get_target(self, target_plugin, path)
+
+        if target[0] == 'unix_socket':
+            try:
+                self.server.unix_target = target[1]
+            except:
+                self.unix_target = target[1]
+        else:
+            try:
+                self.server.unix_target = None
+            except:
+                self.unix_target = None
+        return target
+
+
+def new_ws_proxy():
+    try:
+        os.makedirs(WS_TOKENS_DIR, mode=0755)
+    except OSError as e:
+        if e.errno == errno.EEXIST:
+            pass
+
+    params = {'listen_host': '127.0.0.1',
+              'listen_port': config.get('server', 'websockets_port'),
+              'ssl_only': False}
+
+    # old websockify: do not use TokenFile
+    if not tokenFile:
+        params['target_cfg'] = WS_TOKENS_DIR
+
+    # websockify 0.7 and higher: use TokenFile
+    else:
+        params['token_plugin'] = TokenFile(src=WS_TOKENS_DIR)
+
+    def start_proxy():
+        try:
+            server = WebSocketProxy(RequestHandlerClass=CustomHandler,
+                                    **params)
+        except TypeError:
+            server = CustomHandler(**params)
+
+        server.start_server()
+
+    proc = Process(target=start_proxy)
+    proc.start()
+    return proc
+
+
+def add_proxy_token(name, port, is_unix_socket=False):
+    with open(os.path.join(WS_TOKENS_DIR, name), 'w') as f:
+        """
+        From python documentation base64.urlsafe_b64encode(s)
+        substitutes - instead of + and _ instead of / in the
+        standard Base64 alphabet, BUT the result can still
+        contain = which is not safe in a URL query component.
+        So remove it when needed as base64 can work well without it.
+        """
+        name = base64.urlsafe_b64encode(name).rstrip('=')
+        if is_unix_socket:
+            f.write('%s: unix_socket:%s' % (name.encode('utf-8'), port))
+        else:
+            f.write('%s: localhost:%s' % (name.encode('utf-8'), port))
+
+
+def remove_proxy_token(name):
+    try:
+        os.unlink(os.path.join(WS_TOKENS_DIR, name))
+    except OSError:
+        pass
diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js
index c8c3889..b58706d 100644
--- a/ui/js/src/wok.main.js
+++ b/ui/js/src/wok.main.js
@@ -29,6 +29,46 @@ wok.getConfig(function(result) {
     wok.config = {};
 });
 
+wok.notificationListeners = [];
+wok.addNotificationListener = function(func) {
+    wok.notificationListeners.push(func);
+    $(window).one("hashchange", function() {
+        var del_index = wok.notificationListeners.indexOf(func);
+        wok.notificationListeners.splice(del_index, 1);
+    });
+};
+
+wok.notificationsWebSocket = undefined;
+wok.startNotificationWebSocket = function () {
+    var addr = window.location.hostname + ':' + window.location.port;
+    var token = wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
+    var url = 'wss://' + addr + '/websockify?token=' + token;
+    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
+
+    wok.notificationsWebSocket.onmessage = function(event) {
+        var buffer_rcv = window.atob(event.data);
+        var messages = buffer_rcv.split("//EOM//");
+        for (var i = 0; i < messages.length; i++) {
+            if (messages[i] === "") {
+                continue;
+	    }
+            for (var j = 0; j < wok.notificationListeners.length; j++) {
+                wok.notificationListeners[j](messages[i]);
+            }
+        }
+    };
+
+    sessionStorage.setItem('wokNotificationWebSocket', 'true');
+    var heartbeat = setInterval(function() {
+        wok.notificationsWebSocket.send(window.btoa('heartbeat'));
+    }, 30000);
+
+    wok.notificationsWebSocket.onclose = function() {
+        clearInterval(heartbeat);
+    };
+};
+
+
 wok.main = function() {
     wok.isLoggingOut = false;
     wok.popable();
@@ -395,6 +435,9 @@ wok.main = function() {
 
         // Set handler for help button
         $('#btn-help').on('click', wok.openHelp);
+
+        // start WebSocket
+        wok.startNotificationWebSocket();
     };
 
     var initUI = function() {
diff --git a/ui/js/wok.user-log.js b/ui/js/wok.user-log.js
index 0e8fb09..765b75a 100644
--- a/ui/js/wok.user-log.js
+++ b/ui/js/wok.user-log.js
@@ -153,6 +153,18 @@ wok.initUserLogContent = function() {
         $("#user-log-grid").bootgrid("search");
         wok.initUserLogConfigGridData();
     });
+
+    wok.addNotificationListener(function(message) {
+        var msg_attr = message.split(":");
+        if (msg_attr.length != 2) {
+            return;
+	}
+        var method = msg_attr[0];
+        var target = msg_attr[1];
+        if (method === "POST" && target === '/wok/logs') {
+            $("#refresh-button").click();
+        }
+    });
 };
 
 wok.initUserLogWindow = function() {
-- 
2.9.3

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation
Posted by Aline Manera 7 years, 9 months ago
Hi Daniel,

On 03/07/2017 03:52 PM, dhbarboza82@gmail.com wrote:
> From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
>
> 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 <danielhb@linux.vnet.ibm.com>
> ---
>   contrib/DEBIAN/control.in      |   1 +
>   contrib/wok.spec.fedora.in     |   1 +
>   contrib/wok.spec.suse.in       |   1 +
>   docs/fedora-deps.md            |   2 +-
>   docs/opensuse-deps.md          |   3 +-
>   docs/ubuntu-deps.md            |   3 +-
>   src/wok/config.py.in           |   8 ++
>   src/wok/control/base.py        |  16 ++--
>   src/wok/model/notifications.py |   2 +-
>   src/wok/pushserver.py          | 163 +++++++++++++++++++++++++++++++++++++++++
>   src/wok/reqlogger.py           |  11 ++-
>   src/wok/root.py                |   3 +
>   src/wok/server.py              |  13 +++-
>   src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
>   ui/js/src/wok.main.js          |  43 +++++++++++
>   ui/js/wok.user-log.js          |  12 +++
>   16 files changed, 391 insertions(+), 14 deletions(-)
>   create mode 100644 src/wok/pushserver.py
>   create mode 100644 src/wok/websocket.py

> diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js
> index c8c3889..b58706d 100644
> --- a/ui/js/src/wok.main.js
> +++ b/ui/js/src/wok.main.js
> @@ -29,6 +29,46 @@ wok.getConfig(function(result) {
>       wok.config = {};
>   });

> +wok.notificationListeners = [];
> +wok.addNotificationListener = function(func) {
> +    wok.notificationListeners.push(func);
> +    $(window).one("hashchange", function() {
> +        var del_index = wok.notificationListeners.indexOf(func);
> +        wok.notificationListeners.splice(del_index, 1);
> +    });
> +};
> +
> +wok.notificationsWebSocket = undefined;
> +wok.startNotificationWebSocket = function () {
> +    var addr = window.location.hostname + ':' + window.location.port;
> +    var token = wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
> +    var url = 'wss://' + addr + '/websockify?token=' + token;
> +    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
> +
> +    wok.notificationsWebSocket.onmessage = function(event) {
> +        var buffer_rcv = window.atob(event.data);
> +        var messages = buffer_rcv.split("//EOM//");
> +        for (var i = 0; i < messages.length; i++) {
> +            if (messages[i] === "") {
> +                continue;
> +	    }
> +            for (var j = 0; j < wok.notificationListeners.length; j++) {
> +                wok.notificationListeners[j](messages[i]);
> +            }
> +        }
> +    };
> +

I am not sure I understood this code correctly.
 From what I had in mind wok.notificationListeners should be an dict 
instead of an array. In which the key is the notification message and 
the value the functions to be executed on message received.

wok.notificationListeners = {}

When adding a new listener, we would do:

wok.notificationListeners[<msg>] = <func-to-handle-msg>

That way, we avoid calling each wok.notificationListeners sending the 
same message to listeners not related to that message.

To call the listener, we do:

func = wok.notificationListeners.get(<message>)
if func != undefined:
     #call func
     func()

> +    sessionStorage.setItem('wokNotificationWebSocket', 'true');
> +    var heartbeat = setInterval(function() {
> +        wok.notificationsWebSocket.send(window.btoa('heartbeat'));
> +    }, 30000);
> +
> +    wok.notificationsWebSocket.onclose = function() {
> +        clearInterval(heartbeat);
> +    };
> +};
> +
> +
>   wok.main = function() {
>       wok.isLoggingOut = false;
>       wok.popable();
> @@ -395,6 +435,9 @@ wok.main = function() {
>
>           // Set handler for help button
>           $('#btn-help').on('click', wok.openHelp);
> +
> +        // start WebSocket
> +        wok.startNotificationWebSocket();
>       };
>
>       var initUI = function() {
> diff --git a/ui/js/wok.user-log.js b/ui/js/wok.user-log.js
> index 0e8fb09..765b75a 100644
> --- a/ui/js/wok.user-log.js
> +++ b/ui/js/wok.user-log.js
> @@ -153,6 +153,18 @@ wok.initUserLogContent = function() {
>           $("#user-log-grid").bootgrid("search");
>           wok.initUserLogConfigGridData();
>       });
> +
> +    wok.addNotificationListener(function(message) {

> +        var msg_attr = message.split(":");
> +        if (msg_attr.length != 2) {
> +            return;
> +	}
> +        var method = msg_attr[0];
> +        var target = msg_attr[1];
> +        if (method === "POST" && target === '/wok/logs') {

All the above block is not needed when you follow what I described 
above, as this function would only be called when POST:/wok/logs

> +            $("#refresh-button").click();
> +        }
> +    });
>   };
>
>   wok.initUserLogWindow = function() {

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation
Posted by Daniel Henrique Barboza 7 years, 9 months ago

On 03/10/2017 08:40 AM, Aline Manera wrote:
> Hi Daniel,
>
> On 03/07/2017 03:52 PM, dhbarboza82@gmail.com wrote:
>> From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
>>
>> 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 <danielhb@linux.vnet.ibm.com>
>> ---
>>   contrib/DEBIAN/control.in      |   1 +
>>   contrib/wok.spec.fedora.in     |   1 +
>>   contrib/wok.spec.suse.in       |   1 +
>>   docs/fedora-deps.md            |   2 +-
>>   docs/opensuse-deps.md          |   3 +-
>>   docs/ubuntu-deps.md            |   3 +-
>>   src/wok/config.py.in           |   8 ++
>>   src/wok/control/base.py        |  16 ++--
>>   src/wok/model/notifications.py |   2 +-
>>   src/wok/pushserver.py          | 163 
>> +++++++++++++++++++++++++++++++++++++++++
>>   src/wok/reqlogger.py           |  11 ++-
>>   src/wok/root.py                |   3 +
>>   src/wok/server.py              |  13 +++-
>>   src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
>>   ui/js/src/wok.main.js          |  43 +++++++++++
>>   ui/js/wok.user-log.js          |  12 +++
>>   16 files changed, 391 insertions(+), 14 deletions(-)
>>   create mode 100644 src/wok/pushserver.py
>>   create mode 100644 src/wok/websocket.py
>
>> diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js
>> index c8c3889..b58706d 100644
>> --- a/ui/js/src/wok.main.js
>> +++ b/ui/js/src/wok.main.js
>> @@ -29,6 +29,46 @@ wok.getConfig(function(result) {
>>       wok.config = {};
>>   });
>
>> +wok.notificationListeners = [];
>> +wok.addNotificationListener = function(func) {
>> +    wok.notificationListeners.push(func);
>> +    $(window).one("hashchange", function() {
>> +        var del_index = wok.notificationListeners.indexOf(func);
>> +        wok.notificationListeners.splice(del_index, 1);
>> +    });
>> +};
>> +
>> +wok.notificationsWebSocket = undefined;
>> +wok.startNotificationWebSocket = function () {
>> +    var addr = window.location.hostname + ':' + window.location.port;
>> +    var token = 
>> wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
>> +    var url = 'wss://' + addr + '/websockify?token=' + token;
>> +    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
>> +
>> +    wok.notificationsWebSocket.onmessage = function(event) {
>> +        var buffer_rcv = window.atob(event.data);
>> +        var messages = buffer_rcv.split("//EOM//");
>> +        for (var i = 0; i < messages.length; i++) {
>> +            if (messages[i] === "") {
>> +                continue;
>> +        }
>> +            for (var j = 0; j < wok.notificationListeners.length; 
>> j++) {
>> +                wok.notificationListeners[j](messages[i]);
>> +            }
>> +        }
>> +    };
>> +
>
> I am not sure I understood this code correctly.
> From what I had in mind wok.notificationListeners should be an dict 
> instead of an array. In which the key is the notification message and 
> the value the functions to be executed on message received.
>
> wok.notificationListeners = {}
>
> When adding a new listener, we would do:
>
> wok.notificationListeners[<msg>] = <func-to-handle-msg>
>
> That way, we avoid calling each wok.notificationListeners sending the 
> same message to listeners not related to that message.
>
> To call the listener, we do:
>
> func = wok.notificationListeners.get(<message>)
> if func != undefined:
>     #call func
>     func()

The idea was to make it generic. Every message would be forwarded to all 
functions
that is currently listening to websocket events. The job of filtering 
what you want
would be done inside each listener itself.

This allows for multiple listeners being able to receive the same 
message. Otherwise,
suppose that in the same tab we have multiple independent features (like 
Ginger with
sidebar), common interest messages like "WoK is going to be reloaded" or 
"A task of
id=N has been finished" would be captured by only one of them.


>
>> + sessionStorage.setItem('wokNotificationWebSocket', 'true');
>> +    var heartbeat = setInterval(function() {
>> + wok.notificationsWebSocket.send(window.btoa('heartbeat'));
>> +    }, 30000);
>> +
>> +    wok.notificationsWebSocket.onclose = function() {
>> +        clearInterval(heartbeat);
>> +    };
>> +};
>> +
>> +
>>   wok.main = function() {
>>       wok.isLoggingOut = false;
>>       wok.popable();
>> @@ -395,6 +435,9 @@ wok.main = function() {
>>
>>           // Set handler for help button
>>           $('#btn-help').on('click', wok.openHelp);
>> +
>> +        // start WebSocket
>> +        wok.startNotificationWebSocket();
>>       };
>>
>>       var initUI = function() {
>> diff --git a/ui/js/wok.user-log.js b/ui/js/wok.user-log.js
>> index 0e8fb09..765b75a 100644
>> --- a/ui/js/wok.user-log.js
>> +++ b/ui/js/wok.user-log.js
>> @@ -153,6 +153,18 @@ wok.initUserLogContent = function() {
>>           $("#user-log-grid").bootgrid("search");
>>           wok.initUserLogConfigGridData();
>>       });
>> +
>> +    wok.addNotificationListener(function(message) {
>
>> +        var msg_attr = message.split(":");
>> +        if (msg_attr.length != 2) {
>> +            return;
>> +    }
>> +        var method = msg_attr[0];
>> +        var target = msg_attr[1];
>> +        if (method === "POST" && target === '/wok/logs') {
>
> All the above block is not needed when you follow what I described 
> above, as this function would only be called when POST:/wok/logs

Yes, if we restrict the messages to just be received by one listener 
this filtering is
unnecessary.
>
>> +            $("#refresh-button").click();
>> +        }
>> +    });
>>   };
>>
>>   wok.initUserLogWindow = function() {
>

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation
Posted by Aline Manera 7 years, 9 months ago

On 03/10/2017 09:30 AM, Daniel Henrique Barboza wrote:
>
>
> On 03/10/2017 08:40 AM, Aline Manera wrote:
>> Hi Daniel,
>>
>> On 03/07/2017 03:52 PM, dhbarboza82@gmail.com wrote:
>>> From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
>>>
>>> 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 <danielhb@linux.vnet.ibm.com>
>>> ---
>>>   contrib/DEBIAN/control.in      |   1 +
>>>   contrib/wok.spec.fedora.in     |   1 +
>>>   contrib/wok.spec.suse.in       |   1 +
>>>   docs/fedora-deps.md            |   2 +-
>>>   docs/opensuse-deps.md          |   3 +-
>>>   docs/ubuntu-deps.md            |   3 +-
>>>   src/wok/config.py.in           |   8 ++
>>>   src/wok/control/base.py        |  16 ++--
>>>   src/wok/model/notifications.py |   2 +-
>>>   src/wok/pushserver.py          | 163 
>>> +++++++++++++++++++++++++++++++++++++++++
>>>   src/wok/reqlogger.py           |  11 ++-
>>>   src/wok/root.py                |   3 +
>>>   src/wok/server.py              |  13 +++-
>>>   src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
>>>   ui/js/src/wok.main.js          |  43 +++++++++++
>>>   ui/js/wok.user-log.js          |  12 +++
>>>   16 files changed, 391 insertions(+), 14 deletions(-)
>>>   create mode 100644 src/wok/pushserver.py
>>>   create mode 100644 src/wok/websocket.py
>>
>>> diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js
>>> index c8c3889..b58706d 100644
>>> --- a/ui/js/src/wok.main.js
>>> +++ b/ui/js/src/wok.main.js
>>> @@ -29,6 +29,46 @@ wok.getConfig(function(result) {
>>>       wok.config = {};
>>>   });
>>
>>> +wok.notificationListeners = [];
>>> +wok.addNotificationListener = function(func) {
>>> +    wok.notificationListeners.push(func);
>>> +    $(window).one("hashchange", function() {
>>> +        var del_index = wok.notificationListeners.indexOf(func);
>>> +        wok.notificationListeners.splice(del_index, 1);
>>> +    });
>>> +};
>>> +
>>> +wok.notificationsWebSocket = undefined;
>>> +wok.startNotificationWebSocket = function () {
>>> +    var addr = window.location.hostname + ':' + window.location.port;
>>> +    var token = 
>>> wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
>>> +    var url = 'wss://' + addr + '/websockify?token=' + token;
>>> +    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
>>> +
>>> +    wok.notificationsWebSocket.onmessage = function(event) {
>>> +        var buffer_rcv = window.atob(event.data);
>>> +        var messages = buffer_rcv.split("//EOM//");
>>> +        for (var i = 0; i < messages.length; i++) {
>>> +            if (messages[i] === "") {
>>> +                continue;
>>> +        }
>>> +            for (var j = 0; j < wok.notificationListeners.length; 
>>> j++) {
>>> +                wok.notificationListeners[j](messages[i]);
>>> +            }
>>> +        }
>>> +    };
>>> +
>>
>> I am not sure I understood this code correctly.
>> From what I had in mind wok.notificationListeners should be an dict 
>> instead of an array. In which the key is the notification message and 
>> the value the functions to be executed on message received.
>>
>> wok.notificationListeners = {}
>>
>> When adding a new listener, we would do:
>>
>> wok.notificationListeners[<msg>] = <func-to-handle-msg>
>>
>> That way, we avoid calling each wok.notificationListeners sending the 
>> same message to listeners not related to that message.
>>
>> To call the listener, we do:
>>
>> func = wok.notificationListeners.get(<message>)
>> if func != undefined:
>>     #call func
>>     func()
>
> The idea was to make it generic. Every message would be forwarded to 
> all functions
> that is currently listening to websocket events. The job of filtering 
> what you want
> would be done inside each listener itself.
>
> This allows for multiple listeners being able to receive the same 
> message. Otherwise,
> suppose that in the same tab we have multiple independent features 
> (like Ginger with
> sidebar), common interest messages like "WoK is going to be reloaded" 
> or "A task of
> id=N has been finished" would be captured by only one of them.
>

So do a list of functions to be called.

wok.listenerNotifications[<msg>] = [<list of listener>]

Doesn't make sense forward all the messgaes to all the listener. The 
listener should be trciked by a message.

>
>>
>>> + sessionStorage.setItem('wokNotificationWebSocket', 'true');
>>> +    var heartbeat = setInterval(function() {
>>> + wok.notificationsWebSocket.send(window.btoa('heartbeat'));
>>> +    }, 30000);
>>> +
>>> +    wok.notificationsWebSocket.onclose = function() {
>>> +        clearInterval(heartbeat);
>>> +    };
>>> +};
>>> +
>>> +
>>>   wok.main = function() {
>>>       wok.isLoggingOut = false;
>>>       wok.popable();
>>> @@ -395,6 +435,9 @@ wok.main = function() {
>>>
>>>           // Set handler for help button
>>>           $('#btn-help').on('click', wok.openHelp);
>>> +
>>> +        // start WebSocket
>>> +        wok.startNotificationWebSocket();
>>>       };
>>>
>>>       var initUI = function() {
>>> diff --git a/ui/js/wok.user-log.js b/ui/js/wok.user-log.js
>>> index 0e8fb09..765b75a 100644
>>> --- a/ui/js/wok.user-log.js
>>> +++ b/ui/js/wok.user-log.js
>>> @@ -153,6 +153,18 @@ wok.initUserLogContent = function() {
>>>           $("#user-log-grid").bootgrid("search");
>>>           wok.initUserLogConfigGridData();
>>>       });
>>> +
>>> +    wok.addNotificationListener(function(message) {
>>
>>> +        var msg_attr = message.split(":");
>>> +        if (msg_attr.length != 2) {
>>> +            return;
>>> +    }
>>> +        var method = msg_attr[0];
>>> +        var target = msg_attr[1];
>>> +        if (method === "POST" && target === '/wok/logs') {
>>
>> All the above block is not needed when you follow what I described 
>> above, as this function would only be called when POST:/wok/logs
>
> Yes, if we restrict the messages to just be received by one listener 
> this filtering is
> unnecessary.
>>
>>> + $("#refresh-button").click();
>>> +        }
>>> +    });
>>>   };
>>>
>>>   wok.initUserLogWindow = function() {
>>
>

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel
Re: [Kimchi-devel] [PATCH] [WoK] Asynchronous UI notification implementation
Posted by Aline Manera 7 years, 9 months ago

On 03/10/2017 09:32 AM, Aline Manera wrote:
>
>
> On 03/10/2017 09:30 AM, Daniel Henrique Barboza wrote:
>>
>>
>> On 03/10/2017 08:40 AM, Aline Manera wrote:
>>> Hi Daniel,
>>>
>>> On 03/07/2017 03:52 PM, dhbarboza82@gmail.com wrote:
>>>> From: Daniel Henrique Barboza <danielhb@linux.vnet.ibm.com>
>>>>
>>>> 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 <danielhb@linux.vnet.ibm.com>
>>>> ---
>>>>   contrib/DEBIAN/control.in      |   1 +
>>>>   contrib/wok.spec.fedora.in     |   1 +
>>>>   contrib/wok.spec.suse.in       |   1 +
>>>>   docs/fedora-deps.md            |   2 +-
>>>>   docs/opensuse-deps.md          |   3 +-
>>>>   docs/ubuntu-deps.md            |   3 +-
>>>>   src/wok/config.py.in           |   8 ++
>>>>   src/wok/control/base.py        |  16 ++--
>>>>   src/wok/model/notifications.py |   2 +-
>>>>   src/wok/pushserver.py          | 163 
>>>> +++++++++++++++++++++++++++++++++++++++++
>>>>   src/wok/reqlogger.py           |  11 ++-
>>>>   src/wok/root.py                |   3 +
>>>>   src/wok/server.py              |  13 +++-
>>>>   src/wok/websocket.py           | 123 +++++++++++++++++++++++++++++++
>>>>   ui/js/src/wok.main.js          |  43 +++++++++++
>>>>   ui/js/wok.user-log.js          |  12 +++
>>>>   16 files changed, 391 insertions(+), 14 deletions(-)
>>>>   create mode 100644 src/wok/pushserver.py
>>>>   create mode 100644 src/wok/websocket.py
>>>
>>>> diff --git a/ui/js/src/wok.main.js b/ui/js/src/wok.main.js
>>>> index c8c3889..b58706d 100644
>>>> --- a/ui/js/src/wok.main.js
>>>> +++ b/ui/js/src/wok.main.js
>>>> @@ -29,6 +29,46 @@ wok.getConfig(function(result) {
>>>>       wok.config = {};
>>>>   });
>>>
>>>> +wok.notificationListeners = [];
>>>> +wok.addNotificationListener = function(func) {
>>>> +    wok.notificationListeners.push(func);
>>>> +    $(window).one("hashchange", function() {
>>>> +        var del_index = wok.notificationListeners.indexOf(func);
>>>> +        wok.notificationListeners.splice(del_index, 1);
>>>> +    });
>>>> +};
>>>> +
>>>> +wok.notificationsWebSocket = undefined;
>>>> +wok.startNotificationWebSocket = function () {
>>>> +    var addr = window.location.hostname + ':' + window.location.port;
>>>> +    var token = 
>>>> wok.urlSafeB64Encode('woknotifications').replace(/=*$/g, "");
>>>> +    var url = 'wss://' + addr + '/websockify?token=' + token;
>>>> +    wok.notificationsWebSocket = new WebSocket(url, ['base64']);
>>>> +
>>>> +    wok.notificationsWebSocket.onmessage = function(event) {
>>>> +        var buffer_rcv = window.atob(event.data);
>>>> +        var messages = buffer_rcv.split("//EOM//");
>>>> +        for (var i = 0; i < messages.length; i++) {
>>>> +            if (messages[i] === "") {
>>>> +                continue;
>>>> +        }
>>>> +            for (var j = 0; j < wok.notificationListeners.length; 
>>>> j++) {
>>>> +                wok.notificationListeners[j](messages[i]);
>>>> +            }
>>>> +        }
>>>> +    };
>>>> +
>>>
>>> I am not sure I understood this code correctly.
>>> From what I had in mind wok.notificationListeners should be an dict 
>>> instead of an array. In which the key is the notification message 
>>> and the value the functions to be executed on message received.
>>>
>>> wok.notificationListeners = {}
>>>
>>> When adding a new listener, we would do:
>>>
>>> wok.notificationListeners[<msg>] = <func-to-handle-msg>
>>>
>>> That way, we avoid calling each wok.notificationListeners sending 
>>> the same message to listeners not related to that message.
>>>
>>> To call the listener, we do:
>>>
>>> func = wok.notificationListeners.get(<message>)
>>> if func != undefined:
>>>     #call func
>>>     func()
>>
>> The idea was to make it generic. Every message would be forwarded to 
>> all functions
>> that is currently listening to websocket events. The job of filtering 
>> what you want
>> would be done inside each listener itself.
>>
>> This allows for multiple listeners being able to receive the same 
>> message. Otherwise,
>> suppose that in the same tab we have multiple independent features 
>> (like Ginger with
>> sidebar), common interest messages like "WoK is going to be reloaded" 
>> or "A task of
>> id=N has been finished" would be captured by only one of them.
>>

Also thinking about the Ginger scenario, the $(window).one("hashchange") 
will not work
So it is better to check the function is defined or not.

func_list = wok.listenerNotification[<msg>]
if func_list:
     for func in func_list:
         if func != undefined:
             #call func

>
> So do a list of functions to be called.
>
> wok.listenerNotifications[<msg>] = [<list of listener>]
>
> Doesn't make sense forward all the messgaes to all the listener. The 
> listener should be trciked by a message.
>
>>
>>>
>>>> + sessionStorage.setItem('wokNotificationWebSocket', 'true');
>>>> +    var heartbeat = setInterval(function() {
>>>> + wok.notificationsWebSocket.send(window.btoa('heartbeat'));
>>>> +    }, 30000);
>>>> +
>>>> +    wok.notificationsWebSocket.onclose = function() {
>>>> +        clearInterval(heartbeat);
>>>> +    };
>>>> +};
>>>> +
>>>> +
>>>>   wok.main = function() {
>>>>       wok.isLoggingOut = false;
>>>>       wok.popable();
>>>> @@ -395,6 +435,9 @@ wok.main = function() {
>>>>
>>>>           // Set handler for help button
>>>>           $('#btn-help').on('click', wok.openHelp);
>>>> +
>>>> +        // start WebSocket
>>>> +        wok.startNotificationWebSocket();
>>>>       };
>>>>
>>>>       var initUI = function() {
>>>> diff --git a/ui/js/wok.user-log.js b/ui/js/wok.user-log.js
>>>> index 0e8fb09..765b75a 100644
>>>> --- a/ui/js/wok.user-log.js
>>>> +++ b/ui/js/wok.user-log.js
>>>> @@ -153,6 +153,18 @@ wok.initUserLogContent = function() {
>>>>           $("#user-log-grid").bootgrid("search");
>>>>           wok.initUserLogConfigGridData();
>>>>       });
>>>> +
>>>> +    wok.addNotificationListener(function(message) {
>>>
>>>> +        var msg_attr = message.split(":");
>>>> +        if (msg_attr.length != 2) {
>>>> +            return;
>>>> +    }
>>>> +        var method = msg_attr[0];
>>>> +        var target = msg_attr[1];
>>>> +        if (method === "POST" && target === '/wok/logs') {
>>>
>>> All the above block is not needed when you follow what I described 
>>> above, as this function would only be called when POST:/wok/logs
>>
>> Yes, if we restrict the messages to just be received by one listener 
>> this filtering is
>> unnecessary.
>>>
>>>> + $("#refresh-button").click();
>>>> +        }
>>>> +    });
>>>>   };
>>>>
>>>>   wok.initUserLogWindow = function() {
>>>
>>
>
> _______________________________________________
> Kimchi-devel mailing list
> Kimchi-devel@ovirt.org
> http://lists.ovirt.org/mailman/listinfo/kimchi-devel
>

_______________________________________________
Kimchi-devel mailing list
Kimchi-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/kimchi-devel