blob: bd3e9e16fb104f64179f3aee36975184bbc75e4b [file] [log] [blame]
/*
* Copyright 2015-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
ONOS GUI -- Remote -- Web Socket Service
*/
(function () {
'use strict';
// injected refs
var $log, $loc, fs, gs, ufs, wsock, vs, ls;
// internal state
var webSockOpts, // web socket options
ws = null, // web socket reference
wsUp = false, // web socket is good to go
handlers = {}, // event handler bindings
pendingEvents = [], // events TX'd while socket not up
host, // web socket host
url, // web socket URL
clusterNodes = [], // ONOS instances data for failover
clusterIndex = -1, // the instance to which we are connected
glyphs = [],
connectRetries = 0, // limit our attempts at reconnecting
openListeners = {}, // registered listeners for websocket open()
nextListenerId = 1, // internal ID for open listeners
loggedInUser = null; // name of logged-in user
// built-in handlers
var builtinHandlers = {
bootstrap: function (data) {
$log.debug('bootstrap data', data);
loggedInUser = data.user;
clusterNodes = data.clusterNodes;
clusterNodes.forEach(function (d, i) {
if (d.uiAttached) {
clusterIndex = i;
$log.info('Connected to cluster node ' + d.ip);
// TODO: add connect info to masthead somewhere
}
});
glyphs = data.glyphs;
glyphs.forEach(function (d, i) {
var gdata = {};
gdata['_' + d.id] = d.viewbox;
gdata[d.id] = d.path;
gs.registerGlyphs(gdata);
});
},
error: function (data) {
var m = data.message || 'error from server';
$log.error(m, data);
// Unrecoverable error - throw up the veil...
vs && vs.show([
'Oops!',
'Server reports error...',
m,
]);
},
};
// ==========================
// === Web socket callbacks
function handleOpen() {
$log.info('Web socket open - ', url);
vs && vs.hide();
if (fs.debugOn('txrx')) {
$log.debug('Sending ' + pendingEvents.length + ' pending event(s)...');
}
pendingEvents.forEach(function (ev) {
_send(ev);
});
pendingEvents = [];
connectRetries = 0;
wsUp = true;
informListeners(host, url);
}
// Handles the specified (incoming) message using handler bindings.
function handleMessage(msgEvent) {
var ev, h;
try {
ev = JSON.parse(msgEvent.data);
} catch (e) {
$log.error('Message.data is not valid JSON', msgEvent.data, e);
return null;
}
if (fs.debugOn('txrx')) {
$log.debug(' << *Rx* ', ev.event, ev.payload);
}
if (h = handlers[ev.event]) {
try {
h(ev.payload);
} catch (e) {
$log.error('Problem handling event:', ev, e);
return null;
}
} else {
$log.warn('Unhandled event:', ev);
}
}
function handleClose() {
var gsucc;
$log.warn('Web socket closed');
ls && ls.stop();
wsUp = false;
if (gsucc = findGuiSuccessor()) {
createWebSocket(webSockOpts, gsucc);
} else {
// If no controllers left to contact, show the Veil...
vs && vs.show([
'Oops!',
'Web-socket connection to server closed...',
'Try refreshing the page.',
]);
}
}
// ==============================
// === Private Helper Functions
function findGuiSuccessor() {
var ncn = clusterNodes.length,
ip, node;
while (connectRetries < ncn && !ip) {
connectRetries++;
clusterIndex = (clusterIndex + 1) % ncn;
node = clusterNodes[clusterIndex];
ip = node && node.ip;
}
return ip;
}
function informListeners(host, url) {
angular.forEach(openListeners, function (lsnr) {
lsnr.cb(host, url);
});
}
function _send(ev) {
if (fs.debugOn('txrx')) {
$log.debug(' *Tx* >> ', ev.event, ev.payload);
}
ws.send(JSON.stringify(ev));
}
function noHandlersWarn(handlers, caller) {
if (!handlers || fs.isEmptyObject(handlers)) {
$log.warn('WSS.' + caller + '(): no event handlers');
return true;
}
return false;
}
// ===================
// === API Functions
// Required for unit tests to set to known state
function resetState() {
webSockOpts = undefined;
ws = null;
wsUp = false;
host = undefined;
url = undefined;
pendingEvents = [];
handlers = {};
clusterNodes = [];
clusterIndex = -1;
glyphs = [];
connectRetries = 0;
openListeners = {};
nextListenerId = 1;
}
// Currently supported opts:
// wsport: web socket port (other than default 8181)
// host: if defined, is the host address to use
function createWebSocket(opts, _host_) {
var wsport = (opts && opts.wsport) || null;
webSockOpts = opts; // preserved for future calls
host = _host_ || $loc.host();
url = ufs.wsUrl('core', wsport, _host_);
$log.debug('Attempting to open websocket to: ' + url);
ws = wsock.newWebSocket(url);
if (ws) {
ws.onopen = handleOpen;
ws.onmessage = handleMessage;
ws.onclose = handleClose;
sendEvent('authentication', { token: onosAuth });
}
// Note: Wsock logs an error if the new WebSocket call fails
return url;
}
// Binds the message handlers to their message type (event type) as
// specified in the given map. Note that keys are the event IDs; values
// are either:
// * the event handler function, or
// * an API object which has an event handler for the key
//
function bindHandlers(handlerMap) {
var m,
dups = [];
if (noHandlersWarn(handlerMap, 'bindHandlers')) {
return null;
}
m = d3.map(handlerMap);
m.forEach(function (eventId, api) {
var fn = fs.isF(api) || fs.isF(api[eventId]);
if (!fn) {
$log.warn(eventId + ' handler not a function');
return;
}
if (handlers[eventId]) {
dups.push(eventId);
} else {
handlers[eventId] = fn;
}
});
if (dups.length) {
$log.warn('duplicate bindings ignored:', dups);
}
}
// Unbinds the specified message handlers.
// Expected that the same map will be used, but we only care about keys
function unbindHandlers(handlerMap) {
var m;
if (noHandlersWarn(handlerMap, 'unbindHandlers')) {
return null;
}
m = d3.map(handlerMap);
m.forEach(function (eventId) {
delete handlers[eventId];
});
}
// TODO: simplify listener handling (see theme.js for sample code)
function addOpenListener(callback) {
var id = nextListenerId++,
cb = fs.isF(callback),
o = { id: id, cb: cb };
if (cb) {
openListeners[id] = o;
} else {
$log.error('WSS.addOpenListener(): callback not a function');
o.error = 'No callback defined';
}
return o;
}
function removeOpenListener(lsnr) {
var id = fs.isO(lsnr) && lsnr.id,
o;
if (!id) {
$log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
return null;
}
o = openListeners[id];
if (o) {
delete openListeners[id];
}
}
// Formulates an event message and sends it via the web-socket.
// If the websocket is not up yet, we store it in a pending list.
function sendEvent(evType, payload) {
var ev = {
event: evType,
payload: payload || {},
};
if (wsUp) {
_send(ev);
} else {
pendingEvents.push(ev);
}
}
// Binds the veil service as a delegate
function setVeilDelegate(vd) {
vs = vd;
}
// Binds the loading service as a delegate
function setLoadingDelegate(ld) {
ls = ld;
}
// ============================
// ===== Definition of module
angular.module('onosRemote')
.factory('WebSocketService',
['$log', '$location', 'FnService', 'GlyphService', 'UrlFnService', 'WSock',
function (_$log_, _$loc_, _fs_, _gs_, _ufs_, _wsock_) {
$log = _$log_;
$loc = _$loc_;
fs = _fs_;
gs = _gs_;
ufs = _ufs_;
wsock = _wsock_;
bindHandlers(builtinHandlers);
return {
resetState: resetState,
createWebSocket: createWebSocket,
bindHandlers: bindHandlers,
unbindHandlers: unbindHandlers,
addOpenListener: addOpenListener,
removeOpenListener: removeOpenListener,
sendEvent: sendEvent,
isConnected: function () { return wsUp; },
loggedInUser: function () { return loggedInUser || '(no-one)'; },
_setVeilDelegate: setVeilDelegate,
_setLoadingDelegate: setLoadingDelegate,
};
},
]);
}());