blob: 18dd734418ebfd68093895ff1b3a702285b61f4d [file] [log] [blame]
Sean Condon83fc39f2018-04-19 18:56:13 +01001/*
2 * Copyright 2015-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Sean Condona00bf382018-06-23 07:54:01 +010016import { Injectable, Inject } from '@angular/core';
Sean Condon83fc39f2018-04-19 18:56:13 +010017import { FnService } from '../util/fn.service';
18import { GlyphService } from '../svg/glyph.service';
19import { LogService } from '../../log.service';
20import { UrlFnService } from './urlfn.service';
Sean Condonfd6d11b2018-06-02 20:29:49 +010021import { VeilComponent } from '../layer/veil/veil.component';
Sean Condon83fc39f2018-04-19 18:56:13 +010022import { WSock } from './wsock.service';
23
24/**
25 * Event Type structure for the WebSocketService
26 */
Sean Condonfd6d11b2018-06-02 20:29:49 +010027export interface EventType {
Sean Condon83fc39f2018-04-19 18:56:13 +010028 event: string;
Sean Condonfd6d11b2018-06-02 20:29:49 +010029 payload: Object;
30}
31
32export interface Callback {
33 id: number;
34 error: string;
35 cb(host: string, url: string): void;
36}
37
38interface ClusterNode {
39 id: string;
40 ip: string;
41 m_uiAttached: boolean;
42}
43
44interface Bootstrap {
45 user: string;
46 clusterNodes: ClusterNode[];
47 glyphs: any[]; // TODO: narrow this down to a known type
48}
49
50interface ErrorData {
51 message: string;
52}
53
54export interface WsOptions {
55 wsport: number;
Sean Condon83fc39f2018-04-19 18:56:13 +010056}
57
58/**
59 * ONOS GUI -- Remote -- Web Socket Service
Sean Condonfd6d11b2018-06-02 20:29:49 +010060 *
61 * To see debug messages add ?debug=txrx to the URL
Sean Condon83fc39f2018-04-19 18:56:13 +010062 */
Sean Condonfd6d11b2018-06-02 20:29:49 +010063@Injectable({
64 providedIn: 'root',
65})
Sean Condon83fc39f2018-04-19 18:56:13 +010066export class WebSocketService {
67 // internal state
Sean Condonfd6d11b2018-06-02 20:29:49 +010068 private webSockOpts: WsOptions; // web socket options
69 private ws: WebSocket = null; // web socket reference
70 private wsUp: boolean = false; // web socket is good to go
71
72 // A map of event handler bindings - names and functions (that accept data and return void)
73 private handlers = new Map<string, (data: any) => void>([]);
Sean Condon83fc39f2018-04-19 18:56:13 +010074 private pendingEvents: EventType[] = []; // events TX'd while socket not up
75 private host: string; // web socket host
76 private url; // web socket URL
Sean Condonfd6d11b2018-06-02 20:29:49 +010077 private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
Sean Condon83fc39f2018-04-19 18:56:13 +010078 private clusterIndex = -1; // the instance to which we are connected
79 private glyphs = [];
Sean Condonfd6d11b2018-06-02 20:29:49 +010080 private connectRetries: number = 0; // limit our attempts at reconnecting
Sean Condon83fc39f2018-04-19 18:56:13 +010081
Sean Condonfd6d11b2018-06-02 20:29:49 +010082 // A map of registered Callbacks for websocket open()
83 private openListeners = new Map<number, Callback>([]);
84 private nextListenerId: number = 1; // internal ID for open listeners
85 private loggedInUser = null; // name of logged-in user
86 private lcd: any; // The loading component delegate
87 private vcd: any; // The veil component delegate
88
89 /**
90 * built-in handler for the 'boostrap' event
91 */
92 private bootstrap(data: Bootstrap) {
Sean Condonfd6d11b2018-06-02 20:29:49 +010093 this.loggedInUser = data.user;
94
95 this.clusterNodes = data.clusterNodes;
96 this.clusterNodes.forEach((d, i) => {
97 if (d.m_uiAttached) {
98 this.clusterIndex = i;
99 this.log.info('Connected to cluster node ' + d.ip);
100 // TODO: add connect info to masthead somewhere
101 }
102 });
103 this.glyphs = data.glyphs;
104 const glyphsMap = new Map<string, string>([]);
105 this.glyphs.forEach((d, i) => {
106 glyphsMap.set('_' + d.id, d.viewbox);
107 glyphsMap.set(d.id, d.path);
108 this.gs.registerGlyphs(glyphsMap);
109 });
110 }
111
112 private error(data: ErrorData) {
113 const m: string = data.message || 'error from server';
114 this.log.error(m, data);
115
116 // Unrecoverable error - throw up the veil...
117 if (this.vcd) {
118 this.vcd.show([
119 'Oops!',
120 'Server reports error...',
121 m,
122 ]);
123 }
124 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100125
126 constructor(
127 private fs: FnService,
128 private gs: GlyphService,
129 private log: LogService,
130 private ufs: UrlFnService,
131 private wsock: WSock,
Sean Condona00bf382018-06-23 07:54:01 +0100132 @Inject('Window') private window: Window
Sean Condon83fc39f2018-04-19 18:56:13 +0100133 ) {
134 this.log.debug(window.location.hostname);
Sean Condonfd6d11b2018-06-02 20:29:49 +0100135
136 // Bind the boot strap event by default
137 this.bindHandlers(new Map<string, (data) => void>([
138 ['bootstrap', (data) => this.bootstrap(data)],
139 ['error', (data) => this.error(data)]
140 ]));
141
Sean Condon83fc39f2018-04-19 18:56:13 +0100142 this.log.debug('WebSocketService constructed');
143 }
144
Sean Condonfd6d11b2018-06-02 20:29:49 +0100145
146 // ==========================
147 // === Web socket callbacks
148
149 /**
150 * Called when WebSocket has just opened
151 *
152 * Lift the Veil if it is displayed
153 * If there are any events pending, send them
154 * Mark the WSS as up and inform any listeners for this open event
155 */
156 handleOpen(): void {
157 this.log.info('Web socket open - ', this.url);
158 // Hide the veil
159 if (this.vcd) {
160 this.vcd.hide();
161 }
162
163 if (this.fs.debugOn('txrx')) {
164 this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
165 }
166 this.pendingEvents.forEach((ev) => {
167 this.send(ev);
168 });
169 this.pendingEvents = [];
170
171 this.connectRetries = 0;
172 this.wsUp = true;
173 this.informListeners(this.host, this.url);
174 }
175
176 /**
177 * Function called when WebSocket send a message
178 */
179 handleMessage(msgEvent: MessageEvent): void {
180 let ev: EventType;
181 let h;
182 try {
183 ev = JSON.parse(msgEvent.data);
184 } catch (e) {
185 this.log.error('Message.data is not valid JSON', msgEvent.data, e);
186 return null;
187 }
188 if (this.fs.debugOn('txrx')) {
189 this.log.debug(' << *Rx* ', ev.event, ev.payload);
190 }
191 h = this.handlers.get(ev.event);
192 if (h) {
193 try {
194 h(ev.payload);
195 } catch (e) {
196 this.log.error('Problem handling event:', ev, e);
197 return null;
198 }
199 } else {
200 this.log.warn('Unhandled event:', ev);
201 }
202 }
203
204 /**
205 * Called by the WebSocket if it is closed from the server end
206 *
207 * If the loading component is shown, call stop() on it
208 * Try to find another node in the cluster to connect to
209 * If this is not possible then show the Veil Component
210 */
211 handleClose(): void {
212 this.log.warn('Web socket closed');
213 if (this.lcd) {
214 this.lcd.stop();
215 }
216 this.wsUp = false;
217 let gsucc;
218
219 if (gsucc = this.findGuiSuccessor()) {
220 this.url = this.createWebSocket(this.webSockOpts, gsucc);
221 } else {
222 // If no controllers left to contact, show the Veil...
223 if (this.vcd) {
224 this.vcd.show([
225 'Oops!', // TODO: Localize this
226 'Web-socket connection to server closed...',
227 'Try refreshing the page.',
228 ]);
229 }
230 }
231 }
232
233 // ==============================
234 // === Private Helper Functions
235
236 /**
237 * Find the next node in the ONOS cluster.
238 *
239 * This is used if the WebSocket connection closes because a
240 * node in the cluster ges down - fail over should be automatic
241 */
242 findGuiSuccessor(): string {
243 const ncn = this.clusterNodes.length;
244 let ip: string;
245 let node;
246
247 while (this.connectRetries < ncn && !ip) {
248 this.connectRetries++;
249 this.clusterIndex = (this.clusterIndex + 1) % ncn;
250 node = this.clusterNodes[this.clusterIndex];
251 ip = node && node.ip;
252 }
253
254 return ip;
255 }
256
257 /**
258 * When the WebSocket is opened, inform any listeners that registered
259 * for that event
260 */
261 informListeners(host: string, url: string): void {
262 for (const [key, cb] of this.openListeners.entries()) {
263 cb.cb(host, url);
264 }
265 }
266
267 send(ev: EventType): void {
268 if (this.fs.debugOn('txrx')) {
269 this.log.debug(' *Tx* >> ', ev.event, ev.payload);
270 }
271 this.ws.send(JSON.stringify(ev));
272 }
273
274 /**
275 * Check if there are no WSS event handlers left
276 */
277 noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
278 if (!handlers || handlers.size === 0) {
279 this.log.warn('WSS.' + caller + '(): no event handlers');
280 return true;
281 }
282 return false;
283 }
284
Sean Condon83fc39f2018-04-19 18:56:13 +0100285 /* ===================
286 * === API Functions
Sean Condonfd6d11b2018-06-02 20:29:49 +0100287 */
288
289 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100290 * Required for unit tests to set to known state
291 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100292 resetState(): void {
Sean Condon83fc39f2018-04-19 18:56:13 +0100293 this.webSockOpts = undefined;
294 this.ws = null;
295 this.wsUp = false;
296 this.host = undefined;
297 this.url = undefined;
298 this.pendingEvents = [];
Sean Condonfd6d11b2018-06-02 20:29:49 +0100299 this.handlers.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100300 this.clusterNodes = [];
301 this.clusterIndex = -1;
302 this.glyphs = [];
303 this.connectRetries = 0;
Sean Condonfd6d11b2018-06-02 20:29:49 +0100304 this.openListeners.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100305 this.nextListenerId = 1;
306
307 }
308
Sean Condonfd6d11b2018-06-02 20:29:49 +0100309 /*
310 * Currently supported opts:
Sean Condon83fc39f2018-04-19 18:56:13 +0100311 * wsport: web socket port (other than default 8181)
312 * host: if defined, is the host address to use
313 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100314 createWebSocket(opts?: WsOptions, host?: string) {
Sean Condon83fc39f2018-04-19 18:56:13 +0100315 this.webSockOpts = opts; // preserved for future calls
Sean Condonfd6d11b2018-06-02 20:29:49 +0100316 this.host = host === undefined ? this.window.location.host : host;
317 this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
Sean Condon83fc39f2018-04-19 18:56:13 +0100318
Sean Condonfd6d11b2018-06-02 20:29:49 +0100319 this.log.debug('Attempting to open websocket to: ' + this.url);
320 this.ws = this.wsock.newWebSocket(this.url);
Sean Condon83fc39f2018-04-19 18:56:13 +0100321 if (this.ws) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100322 // fat arrow => syntax means that the 'this' context passed will
323 // be of WebSocketService, not the WebSocket
324 this.ws.onopen = (() => this.handleOpen());
325 this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
326 this.ws.onclose = (() => this.handleClose());
Sean Condon83fc39f2018-04-19 18:56:13 +0100327
Sean Condonfd6d11b2018-06-02 20:29:49 +0100328 this.sendEvent('authentication token', { token: 'testAuth' });
Sean Condon83fc39f2018-04-19 18:56:13 +0100329 }
330 // Note: Wsock logs an error if the new WebSocket call fails
Sean Condonfd6d11b2018-06-02 20:29:49 +0100331 return this.url;
Sean Condon83fc39f2018-04-19 18:56:13 +0100332 }
333
Sean Condon2bd11b72018-06-15 08:00:48 +0100334 /**
335 * Tell the WebSocket to close - this should call the handleClose() method
336 */
337 closeWebSocket() {
338 this.ws.close();
339 }
340
Sean Condonfd6d11b2018-06-02 20:29:49 +0100341
342 /**
343 * Binds the message handlers to their message type (event type) as
344 * specified in the given map. Note that keys are the event IDs; values
345 * are either:
346 * * the event handler function, or
347 * * an API object which has an event handler for the key
348 */
349 bindHandlers(handlerMap: Map<string, (data) => void>): void {
350 const dups: string[] = [];
351
352 if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
353 return null;
354 }
355 for (const [eventId, api] of handlerMap) {
356 this.log.debug('Adding handler for ', eventId);
357 const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
358 if (!fn) {
359 this.log.warn(eventId + ' handler not a function');
360 return;
361 }
362
363 if (this.handlers.get(eventId)) {
364 dups.push(eventId);
365 } else {
366 this.handlers.set(eventId, fn);
367 }
368 }
369 if (dups.length) {
370 this.log.warn('duplicate bindings ignored:', dups);
371 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100372 }
373
Sean Condonfd6d11b2018-06-02 20:29:49 +0100374 /**
375 * Unbinds the specified message handlers.
376 * Expected that the same map will be used, but we only care about keys
377 */
Sean Condon2bd11b72018-06-15 08:00:48 +0100378 unbindHandlers(handlerIds: string[]): void {
379 if ( handlerIds.length === 0 ) {
380 this.log.warn('WSS.unbindHandlers(): no event handlers');
Sean Condonfd6d11b2018-06-02 20:29:49 +0100381 return null;
382 }
Sean Condon2bd11b72018-06-15 08:00:48 +0100383 for (const eventId of handlerIds) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100384 this.handlers.delete(eventId);
385 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100386 }
387
Sean Condonfd6d11b2018-06-02 20:29:49 +0100388 /**
389 * Add a listener function for listening for WebSocket opening.
390 * The function must give a host and url and return void
391 */
392 addOpenListener(callback: (host: string, url: string) => void ): Callback {
393 const id: number = this.nextListenerId++;
394 const cb = this.fs.isF(callback);
395 const o: Callback = <Callback>{ id: id, cb: cb };
396
397 if (cb) {
398 this.openListeners.set(id, o);
399 } else {
400 this.log.error('WSS.addOpenListener(): callback not a function');
401 o.error = 'No callback defined';
402 }
403 return o;
Sean Condon83fc39f2018-04-19 18:56:13 +0100404 }
405
Sean Condonfd6d11b2018-06-02 20:29:49 +0100406 /**
407 * Remove a listener of WebSocket opening
408 */
409 removeOpenListener(lsnr: Callback): void {
410 const id = this.fs.isO(lsnr) && lsnr.id;
411 let o;
412
413 if (!id) {
414 this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
415 return null;
416 }
417 o = this.openListeners[id];
418
419 if (o) {
420 this.openListeners.delete(id);
421 }
422 }
423
424 /**
425 * Formulates an event message and sends it via the web-socket.
Sean Condon83fc39f2018-04-19 18:56:13 +0100426 * If the websocket is not up yet, we store it in a pending list.
427 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100428 sendEvent(evType: string, payload: Object ): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100429 const ev = <EventType> {
Sean Condon83fc39f2018-04-19 18:56:13 +0100430 event: evType,
431 payload: payload
Sean Condon49e15be2018-05-16 16:58:29 +0100432 };
Sean Condon83fc39f2018-04-19 18:56:13 +0100433
434 if (this.wsUp) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100435 this.send(ev);
Sean Condon83fc39f2018-04-19 18:56:13 +0100436 } else {
437 this.pendingEvents.push(ev);
438 }
439 }
440
Sean Condonfd6d11b2018-06-02 20:29:49 +0100441 /**
442 * Binds the veil service as a delegate.
443 */
444 setVeilDelegate(vd: VeilComponent): void {
445 this.vcd = vd;
446 }
447
448 /**
449 * Binds the loading service as a delegate
450 */
451 setLoadingDelegate(ld: any): void {
452 // TODO - Investigate changing Loading Service to LoadingComponent
453 this.log.debug('Loading delegate set', ld);
454 this.lcd = ld;
Sean Condon83fc39f2018-04-19 18:56:13 +0100455 }
456
Sean Condon2bd11b72018-06-15 08:00:48 +0100457 isConnected(): boolean {
458 return this.wsUp;
459 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100460}