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

Daniel Henrique Barboza dhbarboza82 at gmail.com
Fri Mar 10 12:30:03 UTC 2017



On 03/10/2017 08:40 AM, Aline Manera wrote:
> Hi Daniel,
>
> On 03/07/2017 03:52 PM, dhbarboza82 at gmail.com wrote:
>> From: Daniel Henrique Barboza <danielhb at 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 at 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() {
>



More information about the Kimchi-devel mailing list