blob: c45e23fab426f5bcc9856bab2bea9c923e70d736 [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
Sean Condon59d31372019-02-02 20:07:00 +000044interface Glyph {
45 id: string;
46 viewbox: string;
47 path: string;
48}
49
Sean Condonfd6d11b2018-06-02 20:29:49 +010050interface Bootstrap {
51 user: string;
52 clusterNodes: ClusterNode[];
Sean Condon59d31372019-02-02 20:07:00 +000053 glyphs: Glyph[];
Sean Condonfd6d11b2018-06-02 20:29:49 +010054}
55
56interface ErrorData {
57 message: string;
58}
59
60export interface WsOptions {
61 wsport: number;
Sean Condon83fc39f2018-04-19 18:56:13 +010062}
63
64/**
65 * ONOS GUI -- Remote -- Web Socket Service
Sean Condonfd6d11b2018-06-02 20:29:49 +010066 *
67 * To see debug messages add ?debug=txrx to the URL
Sean Condon83fc39f2018-04-19 18:56:13 +010068 */
Sean Condonfd6d11b2018-06-02 20:29:49 +010069@Injectable({
70 providedIn: 'root',
71})
Sean Condon83fc39f2018-04-19 18:56:13 +010072export class WebSocketService {
73 // internal state
Sean Condonfd6d11b2018-06-02 20:29:49 +010074 private webSockOpts: WsOptions; // web socket options
75 private ws: WebSocket = null; // web socket reference
76 private wsUp: boolean = false; // web socket is good to go
77
78 // A map of event handler bindings - names and functions (that accept data and return void)
79 private handlers = new Map<string, (data: any) => void>([]);
Sean Condon83fc39f2018-04-19 18:56:13 +010080 private pendingEvents: EventType[] = []; // events TX'd while socket not up
81 private host: string; // web socket host
82 private url; // web socket URL
Sean Condonfd6d11b2018-06-02 20:29:49 +010083 private clusterNodes: ClusterNode[] = []; // ONOS instances data for failover
Sean Condon83fc39f2018-04-19 18:56:13 +010084 private clusterIndex = -1; // the instance to which we are connected
Sean Condon59d31372019-02-02 20:07:00 +000085 private glyphs: Glyph[] = [];
Sean Condonfd6d11b2018-06-02 20:29:49 +010086 private connectRetries: number = 0; // limit our attempts at reconnecting
Sean Condon83fc39f2018-04-19 18:56:13 +010087
Sean Condonfd6d11b2018-06-02 20:29:49 +010088 // A map of registered Callbacks for websocket open()
89 private openListeners = new Map<number, Callback>([]);
90 private nextListenerId: number = 1; // internal ID for open listeners
91 private loggedInUser = null; // name of logged-in user
92 private lcd: any; // The loading component delegate
93 private vcd: any; // The veil component delegate
94
95 /**
96 * built-in handler for the 'boostrap' event
97 */
98 private bootstrap(data: Bootstrap) {
Sean Condonfd6d11b2018-06-02 20:29:49 +010099 this.loggedInUser = data.user;
Sean Condonb2c483c2019-01-16 20:28:55 +0000100 this.log.info('Websocket connection bootstraped', data);
Sean Condonfd6d11b2018-06-02 20:29:49 +0100101
102 this.clusterNodes = data.clusterNodes;
103 this.clusterNodes.forEach((d, i) => {
104 if (d.m_uiAttached) {
105 this.clusterIndex = i;
106 this.log.info('Connected to cluster node ' + d.ip);
107 // TODO: add connect info to masthead somewhere
108 }
109 });
110 this.glyphs = data.glyphs;
111 const glyphsMap = new Map<string, string>([]);
Sean Condon59d31372019-02-02 20:07:00 +0000112 this.glyphs.forEach((d) => {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100113 glyphsMap.set('_' + d.id, d.viewbox);
114 glyphsMap.set(d.id, d.path);
115 this.gs.registerGlyphs(glyphsMap);
116 });
117 }
118
119 private error(data: ErrorData) {
120 const m: string = data.message || 'error from server';
121 this.log.error(m, data);
122
123 // Unrecoverable error - throw up the veil...
124 if (this.vcd) {
125 this.vcd.show([
126 'Oops!',
127 'Server reports error...',
128 m,
129 ]);
130 }
131 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100132
133 constructor(
134 private fs: FnService,
135 private gs: GlyphService,
136 private log: LogService,
137 private ufs: UrlFnService,
138 private wsock: WSock,
Sean Condon5ca00262018-09-06 17:55:25 +0100139 @Inject('Window') private window: any
Sean Condon83fc39f2018-04-19 18:56:13 +0100140 ) {
141 this.log.debug(window.location.hostname);
Sean Condonfd6d11b2018-06-02 20:29:49 +0100142
143 // Bind the boot strap event by default
144 this.bindHandlers(new Map<string, (data) => void>([
145 ['bootstrap', (data) => this.bootstrap(data)],
146 ['error', (data) => this.error(data)]
147 ]));
148
Sean Condon83fc39f2018-04-19 18:56:13 +0100149 this.log.debug('WebSocketService constructed');
150 }
151
Sean Condonfd6d11b2018-06-02 20:29:49 +0100152
153 // ==========================
154 // === Web socket callbacks
155
156 /**
157 * Called when WebSocket has just opened
158 *
159 * Lift the Veil if it is displayed
160 * If there are any events pending, send them
161 * Mark the WSS as up and inform any listeners for this open event
162 */
163 handleOpen(): void {
164 this.log.info('Web socket open - ', this.url);
165 // Hide the veil
166 if (this.vcd) {
167 this.vcd.hide();
168 }
169
170 if (this.fs.debugOn('txrx')) {
171 this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
172 }
173 this.pendingEvents.forEach((ev) => {
174 this.send(ev);
175 });
176 this.pendingEvents = [];
177
178 this.connectRetries = 0;
179 this.wsUp = true;
180 this.informListeners(this.host, this.url);
181 }
182
183 /**
184 * Function called when WebSocket send a message
185 */
186 handleMessage(msgEvent: MessageEvent): void {
187 let ev: EventType;
188 let h;
189 try {
Sean Condon436c60a2021-01-01 14:23:29 +0000190 ev = JSON.parse(msgEvent.data.toString()) as EventType;
Sean Condonfd6d11b2018-06-02 20:29:49 +0100191 } catch (e) {
192 this.log.error('Message.data is not valid JSON', msgEvent.data, e);
193 return null;
194 }
195 if (this.fs.debugOn('txrx')) {
196 this.log.debug(' << *Rx* ', ev.event, ev.payload);
197 }
198 h = this.handlers.get(ev.event);
199 if (h) {
200 try {
201 h(ev.payload);
202 } catch (e) {
203 this.log.error('Problem handling event:', ev, e);
204 return null;
205 }
206 } else {
207 this.log.warn('Unhandled event:', ev);
208 }
209 }
210
211 /**
212 * Called by the WebSocket if it is closed from the server end
213 *
214 * If the loading component is shown, call stop() on it
215 * Try to find another node in the cluster to connect to
216 * If this is not possible then show the Veil Component
217 */
218 handleClose(): void {
219 this.log.warn('Web socket closed');
220 if (this.lcd) {
221 this.lcd.stop();
222 }
223 this.wsUp = false;
224 let gsucc;
225
226 if (gsucc = this.findGuiSuccessor()) {
227 this.url = this.createWebSocket(this.webSockOpts, gsucc);
228 } else {
229 // If no controllers left to contact, show the Veil...
230 if (this.vcd) {
231 this.vcd.show([
232 'Oops!', // TODO: Localize this
233 'Web-socket connection to server closed...',
234 'Try refreshing the page.',
235 ]);
236 }
237 }
238 }
239
240 // ==============================
241 // === Private Helper Functions
242
243 /**
244 * Find the next node in the ONOS cluster.
245 *
246 * This is used if the WebSocket connection closes because a
247 * node in the cluster ges down - fail over should be automatic
248 */
249 findGuiSuccessor(): string {
250 const ncn = this.clusterNodes.length;
251 let ip: string;
252 let node;
253
254 while (this.connectRetries < ncn && !ip) {
255 this.connectRetries++;
256 this.clusterIndex = (this.clusterIndex + 1) % ncn;
257 node = this.clusterNodes[this.clusterIndex];
258 ip = node && node.ip;
259 }
260
261 return ip;
262 }
263
264 /**
265 * When the WebSocket is opened, inform any listeners that registered
266 * for that event
267 */
268 informListeners(host: string, url: string): void {
269 for (const [key, cb] of this.openListeners.entries()) {
270 cb.cb(host, url);
271 }
272 }
273
274 send(ev: EventType): void {
275 if (this.fs.debugOn('txrx')) {
276 this.log.debug(' *Tx* >> ', ev.event, ev.payload);
277 }
278 this.ws.send(JSON.stringify(ev));
279 }
280
281 /**
282 * Check if there are no WSS event handlers left
283 */
284 noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
285 if (!handlers || handlers.size === 0) {
286 this.log.warn('WSS.' + caller + '(): no event handlers');
287 return true;
288 }
289 return false;
290 }
291
Sean Condon83fc39f2018-04-19 18:56:13 +0100292 /* ===================
293 * === API Functions
Sean Condonfd6d11b2018-06-02 20:29:49 +0100294 */
295
296 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100297 * Required for unit tests to set to known state
298 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100299 resetState(): void {
Sean Condon83fc39f2018-04-19 18:56:13 +0100300 this.webSockOpts = undefined;
301 this.ws = null;
302 this.wsUp = false;
303 this.host = undefined;
304 this.url = undefined;
305 this.pendingEvents = [];
Sean Condonfd6d11b2018-06-02 20:29:49 +0100306 this.handlers.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100307 this.clusterNodes = [];
308 this.clusterIndex = -1;
309 this.glyphs = [];
310 this.connectRetries = 0;
Sean Condonfd6d11b2018-06-02 20:29:49 +0100311 this.openListeners.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100312 this.nextListenerId = 1;
313
314 }
315
Sean Condonfd6d11b2018-06-02 20:29:49 +0100316 /*
317 * Currently supported opts:
Sean Condon83fc39f2018-04-19 18:56:13 +0100318 * wsport: web socket port (other than default 8181)
319 * host: if defined, is the host address to use
320 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100321 createWebSocket(opts?: WsOptions, host?: string) {
Sean Condon83fc39f2018-04-19 18:56:13 +0100322 this.webSockOpts = opts; // preserved for future calls
Sean Condonfd6d11b2018-06-02 20:29:49 +0100323 this.host = host === undefined ? this.window.location.host : host;
324 this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
Sean Condon83fc39f2018-04-19 18:56:13 +0100325
Sean Condonfd6d11b2018-06-02 20:29:49 +0100326 this.log.debug('Attempting to open websocket to: ' + this.url);
327 this.ws = this.wsock.newWebSocket(this.url);
Sean Condon83fc39f2018-04-19 18:56:13 +0100328 if (this.ws) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100329 // fat arrow => syntax means that the 'this' context passed will
330 // be of WebSocketService, not the WebSocket
331 this.ws.onopen = (() => this.handleOpen());
332 this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
333 this.ws.onclose = (() => this.handleClose());
Sean Condone820e9b2018-08-16 15:43:19 +0100334 const authToken = this.window['onosAuth'];
335 this.log.debug('Auth Token for opening WebSocket', authToken);
336 this.sendEvent('authentication', { token: authToken });
Sean Condon83fc39f2018-04-19 18:56:13 +0100337 }
338 // Note: Wsock logs an error if the new WebSocket call fails
Sean Condonfd6d11b2018-06-02 20:29:49 +0100339 return this.url;
Sean Condon83fc39f2018-04-19 18:56:13 +0100340 }
341
Sean Condon2bd11b72018-06-15 08:00:48 +0100342 /**
343 * Tell the WebSocket to close - this should call the handleClose() method
344 */
345 closeWebSocket() {
346 this.ws.close();
347 }
348
Sean Condonfd6d11b2018-06-02 20:29:49 +0100349
350 /**
351 * Binds the message handlers to their message type (event type) as
352 * specified in the given map. Note that keys are the event IDs; values
353 * are either:
354 * * the event handler function, or
355 * * an API object which has an event handler for the key
356 */
357 bindHandlers(handlerMap: Map<string, (data) => void>): void {
358 const dups: string[] = [];
359
360 if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
361 return null;
362 }
363 for (const [eventId, api] of handlerMap) {
364 this.log.debug('Adding handler for ', eventId);
365 const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
366 if (!fn) {
367 this.log.warn(eventId + ' handler not a function');
368 return;
369 }
370
371 if (this.handlers.get(eventId)) {
372 dups.push(eventId);
373 } else {
374 this.handlers.set(eventId, fn);
375 }
376 }
377 if (dups.length) {
378 this.log.warn('duplicate bindings ignored:', dups);
379 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100380 }
381
Sean Condonfd6d11b2018-06-02 20:29:49 +0100382 /**
383 * Unbinds the specified message handlers.
384 * Expected that the same map will be used, but we only care about keys
385 */
Sean Condon2bd11b72018-06-15 08:00:48 +0100386 unbindHandlers(handlerIds: string[]): void {
387 if ( handlerIds.length === 0 ) {
388 this.log.warn('WSS.unbindHandlers(): no event handlers');
Sean Condonfd6d11b2018-06-02 20:29:49 +0100389 return null;
390 }
Sean Condon2bd11b72018-06-15 08:00:48 +0100391 for (const eventId of handlerIds) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100392 this.handlers.delete(eventId);
393 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100394 }
395
Sean Condon28ecc5f2018-06-25 12:50:16 +0100396 isHandling(handlerId: string): boolean {
397 return this.handlers.get(handlerId) !== undefined;
398 }
399
Sean Condonfd6d11b2018-06-02 20:29:49 +0100400 /**
401 * Add a listener function for listening for WebSocket opening.
402 * The function must give a host and url and return void
403 */
404 addOpenListener(callback: (host: string, url: string) => void ): Callback {
405 const id: number = this.nextListenerId++;
406 const cb = this.fs.isF(callback);
407 const o: Callback = <Callback>{ id: id, cb: cb };
408
409 if (cb) {
410 this.openListeners.set(id, o);
411 } else {
412 this.log.error('WSS.addOpenListener(): callback not a function');
413 o.error = 'No callback defined';
414 }
415 return o;
Sean Condon83fc39f2018-04-19 18:56:13 +0100416 }
417
Sean Condonfd6d11b2018-06-02 20:29:49 +0100418 /**
419 * Remove a listener of WebSocket opening
420 */
421 removeOpenListener(lsnr: Callback): void {
422 const id = this.fs.isO(lsnr) && lsnr.id;
423 let o;
424
425 if (!id) {
426 this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
427 return null;
428 }
429 o = this.openListeners[id];
430
431 if (o) {
432 this.openListeners.delete(id);
433 }
434 }
435
436 /**
437 * Formulates an event message and sends it via the web-socket.
Sean Condon83fc39f2018-04-19 18:56:13 +0100438 * If the websocket is not up yet, we store it in a pending list.
439 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100440 sendEvent(evType: string, payload: Object ): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100441 const ev = <EventType> {
Sean Condon83fc39f2018-04-19 18:56:13 +0100442 event: evType,
443 payload: payload
Sean Condon49e15be2018-05-16 16:58:29 +0100444 };
Sean Condon83fc39f2018-04-19 18:56:13 +0100445
446 if (this.wsUp) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100447 this.send(ev);
Sean Condon83fc39f2018-04-19 18:56:13 +0100448 } else {
449 this.pendingEvents.push(ev);
450 }
451 }
452
Sean Condonfd6d11b2018-06-02 20:29:49 +0100453 /**
454 * Binds the veil service as a delegate.
455 */
456 setVeilDelegate(vd: VeilComponent): void {
457 this.vcd = vd;
458 }
459
460 /**
461 * Binds the loading service as a delegate
462 */
463 setLoadingDelegate(ld: any): void {
464 // TODO - Investigate changing Loading Service to LoadingComponent
465 this.log.debug('Loading delegate set', ld);
466 this.lcd = ld;
Sean Condon83fc39f2018-04-19 18:56:13 +0100467 }
468
Sean Condon2bd11b72018-06-15 08:00:48 +0100469 isConnected(): boolean {
470 return this.wsUp;
471 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100472}