blob: c4d5227f6b108b511ed813de7781e603d867c6e3 [file] [log] [blame]
Sean Condon83fc39f2018-04-19 18:56:13 +01001/*
Sean Condon5ca00262018-09-06 17:55:25 +01002 * Copyright 2018-present Open Networking Foundation
Sean Condon83fc39f2018-04-19 18:56:13 +01003 *
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';
Sean Condon5ca00262018-09-06 17:55:25 +010019import { LogService } from '../log.service';
Sean Condon83fc39f2018-04-19 18:56:13 +010020import { 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 Condon5ca00262018-09-06 17:55:25 +0100132 @Inject('Window') private window: any
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 Condone820e9b2018-08-16 15:43:19 +0100327 const authToken = this.window['onosAuth'];
328 this.log.debug('Auth Token for opening WebSocket', authToken);
329 this.sendEvent('authentication', { token: authToken });
Sean Condon83fc39f2018-04-19 18:56:13 +0100330 }
331 // Note: Wsock logs an error if the new WebSocket call fails
Sean Condonfd6d11b2018-06-02 20:29:49 +0100332 return this.url;
Sean Condon83fc39f2018-04-19 18:56:13 +0100333 }
334
Sean Condon2bd11b72018-06-15 08:00:48 +0100335 /**
336 * Tell the WebSocket to close - this should call the handleClose() method
337 */
338 closeWebSocket() {
339 this.ws.close();
340 }
341
Sean Condonfd6d11b2018-06-02 20:29:49 +0100342
343 /**
344 * Binds the message handlers to their message type (event type) as
345 * specified in the given map. Note that keys are the event IDs; values
346 * are either:
347 * * the event handler function, or
348 * * an API object which has an event handler for the key
349 */
350 bindHandlers(handlerMap: Map<string, (data) => void>): void {
351 const dups: string[] = [];
352
353 if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
354 return null;
355 }
356 for (const [eventId, api] of handlerMap) {
357 this.log.debug('Adding handler for ', eventId);
358 const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
359 if (!fn) {
360 this.log.warn(eventId + ' handler not a function');
361 return;
362 }
363
364 if (this.handlers.get(eventId)) {
365 dups.push(eventId);
366 } else {
367 this.handlers.set(eventId, fn);
368 }
369 }
370 if (dups.length) {
371 this.log.warn('duplicate bindings ignored:', dups);
372 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100373 }
374
Sean Condonfd6d11b2018-06-02 20:29:49 +0100375 /**
376 * Unbinds the specified message handlers.
377 * Expected that the same map will be used, but we only care about keys
378 */
Sean Condon2bd11b72018-06-15 08:00:48 +0100379 unbindHandlers(handlerIds: string[]): void {
380 if ( handlerIds.length === 0 ) {
381 this.log.warn('WSS.unbindHandlers(): no event handlers');
Sean Condonfd6d11b2018-06-02 20:29:49 +0100382 return null;
383 }
Sean Condon2bd11b72018-06-15 08:00:48 +0100384 for (const eventId of handlerIds) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100385 this.handlers.delete(eventId);
386 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100387 }
388
Sean Condon28ecc5f2018-06-25 12:50:16 +0100389 isHandling(handlerId: string): boolean {
390 return this.handlers.get(handlerId) !== undefined;
391 }
392
Sean Condonfd6d11b2018-06-02 20:29:49 +0100393 /**
394 * Add a listener function for listening for WebSocket opening.
395 * The function must give a host and url and return void
396 */
397 addOpenListener(callback: (host: string, url: string) => void ): Callback {
398 const id: number = this.nextListenerId++;
399 const cb = this.fs.isF(callback);
400 const o: Callback = <Callback>{ id: id, cb: cb };
401
402 if (cb) {
403 this.openListeners.set(id, o);
404 } else {
405 this.log.error('WSS.addOpenListener(): callback not a function');
406 o.error = 'No callback defined';
407 }
408 return o;
Sean Condon83fc39f2018-04-19 18:56:13 +0100409 }
410
Sean Condonfd6d11b2018-06-02 20:29:49 +0100411 /**
412 * Remove a listener of WebSocket opening
413 */
414 removeOpenListener(lsnr: Callback): void {
415 const id = this.fs.isO(lsnr) && lsnr.id;
416 let o;
417
418 if (!id) {
419 this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
420 return null;
421 }
422 o = this.openListeners[id];
423
424 if (o) {
425 this.openListeners.delete(id);
426 }
427 }
428
429 /**
430 * Formulates an event message and sends it via the web-socket.
Sean Condon83fc39f2018-04-19 18:56:13 +0100431 * If the websocket is not up yet, we store it in a pending list.
432 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100433 sendEvent(evType: string, payload: Object ): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100434 const ev = <EventType> {
Sean Condon83fc39f2018-04-19 18:56:13 +0100435 event: evType,
436 payload: payload
Sean Condon49e15be2018-05-16 16:58:29 +0100437 };
Sean Condon83fc39f2018-04-19 18:56:13 +0100438
439 if (this.wsUp) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100440 this.send(ev);
Sean Condon83fc39f2018-04-19 18:56:13 +0100441 } else {
442 this.pendingEvents.push(ev);
443 }
444 }
445
Sean Condonfd6d11b2018-06-02 20:29:49 +0100446 /**
447 * Binds the veil service as a delegate.
448 */
449 setVeilDelegate(vd: VeilComponent): void {
450 this.vcd = vd;
451 }
452
453 /**
454 * Binds the loading service as a delegate
455 */
456 setLoadingDelegate(ld: any): void {
457 // TODO - Investigate changing Loading Service to LoadingComponent
458 this.log.debug('Loading delegate set', ld);
459 this.lcd = ld;
Sean Condon83fc39f2018-04-19 18:56:13 +0100460 }
461
Sean Condon2bd11b72018-06-15 08:00:48 +0100462 isConnected(): boolean {
463 return this.wsUp;
464 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100465}