blob: e470ec1ef06afe2a2fc86fb75b5d8a90a4b5667e [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 */
16import { Injectable } from '@angular/core';
17import { 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) {
93 this.log.debug('bootstrap data', data);
94 this.loggedInUser = data.user;
95
96 this.clusterNodes = data.clusterNodes;
97 this.clusterNodes.forEach((d, i) => {
98 if (d.m_uiAttached) {
99 this.clusterIndex = i;
100 this.log.info('Connected to cluster node ' + d.ip);
101 // TODO: add connect info to masthead somewhere
102 }
103 });
104 this.glyphs = data.glyphs;
105 const glyphsMap = new Map<string, string>([]);
106 this.glyphs.forEach((d, i) => {
107 glyphsMap.set('_' + d.id, d.viewbox);
108 glyphsMap.set(d.id, d.path);
109 this.gs.registerGlyphs(glyphsMap);
110 });
111 }
112
113 private error(data: ErrorData) {
114 const m: string = data.message || 'error from server';
115 this.log.error(m, data);
116
117 // Unrecoverable error - throw up the veil...
118 if (this.vcd) {
119 this.vcd.show([
120 'Oops!',
121 'Server reports error...',
122 m,
123 ]);
124 }
125 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100126
127 constructor(
128 private fs: FnService,
129 private gs: GlyphService,
130 private log: LogService,
131 private ufs: UrlFnService,
132 private wsock: WSock,
133 private window: Window
134 ) {
135 this.log.debug(window.location.hostname);
Sean Condonfd6d11b2018-06-02 20:29:49 +0100136
137 // Bind the boot strap event by default
138 this.bindHandlers(new Map<string, (data) => void>([
139 ['bootstrap', (data) => this.bootstrap(data)],
140 ['error', (data) => this.error(data)]
141 ]));
142
Sean Condon83fc39f2018-04-19 18:56:13 +0100143 this.log.debug('WebSocketService constructed');
144 }
145
Sean Condonfd6d11b2018-06-02 20:29:49 +0100146
147 // ==========================
148 // === Web socket callbacks
149
150 /**
151 * Called when WebSocket has just opened
152 *
153 * Lift the Veil if it is displayed
154 * If there are any events pending, send them
155 * Mark the WSS as up and inform any listeners for this open event
156 */
157 handleOpen(): void {
158 this.log.info('Web socket open - ', this.url);
159 // Hide the veil
160 if (this.vcd) {
161 this.vcd.hide();
162 }
163
164 if (this.fs.debugOn('txrx')) {
165 this.log.debug('Sending ' + this.pendingEvents.length + ' pending event(s)...');
166 }
167 this.pendingEvents.forEach((ev) => {
168 this.send(ev);
169 });
170 this.pendingEvents = [];
171
172 this.connectRetries = 0;
173 this.wsUp = true;
174 this.informListeners(this.host, this.url);
175 }
176
177 /**
178 * Function called when WebSocket send a message
179 */
180 handleMessage(msgEvent: MessageEvent): void {
181 let ev: EventType;
182 let h;
183 try {
184 ev = JSON.parse(msgEvent.data);
185 } catch (e) {
186 this.log.error('Message.data is not valid JSON', msgEvent.data, e);
187 return null;
188 }
189 if (this.fs.debugOn('txrx')) {
190 this.log.debug(' << *Rx* ', ev.event, ev.payload);
191 }
192 h = this.handlers.get(ev.event);
193 if (h) {
194 try {
195 h(ev.payload);
196 } catch (e) {
197 this.log.error('Problem handling event:', ev, e);
198 return null;
199 }
200 } else {
201 this.log.warn('Unhandled event:', ev);
202 }
203 }
204
205 /**
206 * Called by the WebSocket if it is closed from the server end
207 *
208 * If the loading component is shown, call stop() on it
209 * Try to find another node in the cluster to connect to
210 * If this is not possible then show the Veil Component
211 */
212 handleClose(): void {
213 this.log.warn('Web socket closed');
214 if (this.lcd) {
215 this.lcd.stop();
216 }
217 this.wsUp = false;
218 let gsucc;
219
220 if (gsucc = this.findGuiSuccessor()) {
221 this.url = this.createWebSocket(this.webSockOpts, gsucc);
222 } else {
223 // If no controllers left to contact, show the Veil...
224 if (this.vcd) {
225 this.vcd.show([
226 'Oops!', // TODO: Localize this
227 'Web-socket connection to server closed...',
228 'Try refreshing the page.',
229 ]);
230 }
231 }
232 }
233
234 // ==============================
235 // === Private Helper Functions
236
237 /**
238 * Find the next node in the ONOS cluster.
239 *
240 * This is used if the WebSocket connection closes because a
241 * node in the cluster ges down - fail over should be automatic
242 */
243 findGuiSuccessor(): string {
244 const ncn = this.clusterNodes.length;
245 let ip: string;
246 let node;
247
248 while (this.connectRetries < ncn && !ip) {
249 this.connectRetries++;
250 this.clusterIndex = (this.clusterIndex + 1) % ncn;
251 node = this.clusterNodes[this.clusterIndex];
252 ip = node && node.ip;
253 }
254
255 return ip;
256 }
257
258 /**
259 * When the WebSocket is opened, inform any listeners that registered
260 * for that event
261 */
262 informListeners(host: string, url: string): void {
263 for (const [key, cb] of this.openListeners.entries()) {
264 cb.cb(host, url);
265 }
266 }
267
268 send(ev: EventType): void {
269 if (this.fs.debugOn('txrx')) {
270 this.log.debug(' *Tx* >> ', ev.event, ev.payload);
271 }
272 this.ws.send(JSON.stringify(ev));
273 }
274
275 /**
276 * Check if there are no WSS event handlers left
277 */
278 noHandlersWarn(handlers: Map<string, Object>, caller: string): boolean {
279 if (!handlers || handlers.size === 0) {
280 this.log.warn('WSS.' + caller + '(): no event handlers');
281 return true;
282 }
283 return false;
284 }
285
Sean Condon83fc39f2018-04-19 18:56:13 +0100286 /* ===================
287 * === API Functions
Sean Condonfd6d11b2018-06-02 20:29:49 +0100288 */
289
290 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100291 * Required for unit tests to set to known state
292 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100293 resetState(): void {
Sean Condon83fc39f2018-04-19 18:56:13 +0100294 this.webSockOpts = undefined;
295 this.ws = null;
296 this.wsUp = false;
297 this.host = undefined;
298 this.url = undefined;
299 this.pendingEvents = [];
Sean Condonfd6d11b2018-06-02 20:29:49 +0100300 this.handlers.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100301 this.clusterNodes = [];
302 this.clusterIndex = -1;
303 this.glyphs = [];
304 this.connectRetries = 0;
Sean Condonfd6d11b2018-06-02 20:29:49 +0100305 this.openListeners.clear();
Sean Condon83fc39f2018-04-19 18:56:13 +0100306 this.nextListenerId = 1;
307
308 }
309
Sean Condonfd6d11b2018-06-02 20:29:49 +0100310 /*
311 * Currently supported opts:
Sean Condon83fc39f2018-04-19 18:56:13 +0100312 * wsport: web socket port (other than default 8181)
313 * host: if defined, is the host address to use
314 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100315 createWebSocket(opts?: WsOptions, host?: string) {
Sean Condon83fc39f2018-04-19 18:56:13 +0100316 this.webSockOpts = opts; // preserved for future calls
Sean Condonfd6d11b2018-06-02 20:29:49 +0100317 this.host = host === undefined ? this.window.location.host : host;
318 this.url = this.ufs.wsUrl('core', opts === undefined ? '' : opts['wsport'].toString(), host);
Sean Condon83fc39f2018-04-19 18:56:13 +0100319
Sean Condonfd6d11b2018-06-02 20:29:49 +0100320 this.log.debug('Attempting to open websocket to: ' + this.url);
321 this.ws = this.wsock.newWebSocket(this.url);
Sean Condon83fc39f2018-04-19 18:56:13 +0100322 if (this.ws) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100323 // fat arrow => syntax means that the 'this' context passed will
324 // be of WebSocketService, not the WebSocket
325 this.ws.onopen = (() => this.handleOpen());
326 this.ws.onmessage = ((msgEvent) => this.handleMessage(msgEvent));
327 this.ws.onclose = (() => this.handleClose());
Sean Condon83fc39f2018-04-19 18:56:13 +0100328
Sean Condonfd6d11b2018-06-02 20:29:49 +0100329 this.sendEvent('authentication token', { token: 'testAuth' });
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 Condonfd6d11b2018-06-02 20:29:49 +0100335
336 /**
337 * Binds the message handlers to their message type (event type) as
338 * specified in the given map. Note that keys are the event IDs; values
339 * are either:
340 * * the event handler function, or
341 * * an API object which has an event handler for the key
342 */
343 bindHandlers(handlerMap: Map<string, (data) => void>): void {
344 const dups: string[] = [];
345
346 if (this.noHandlersWarn(handlerMap, 'bindHandlers')) {
347 return null;
348 }
349 for (const [eventId, api] of handlerMap) {
350 this.log.debug('Adding handler for ', eventId);
351 const fn = this.fs.isF(api) || this.fs.isF(api[eventId]);
352 if (!fn) {
353 this.log.warn(eventId + ' handler not a function');
354 return;
355 }
356
357 if (this.handlers.get(eventId)) {
358 dups.push(eventId);
359 } else {
360 this.handlers.set(eventId, fn);
361 }
362 }
363 if (dups.length) {
364 this.log.warn('duplicate bindings ignored:', dups);
365 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100366 }
367
Sean Condonfd6d11b2018-06-02 20:29:49 +0100368 /**
369 * Unbinds the specified message handlers.
370 * Expected that the same map will be used, but we only care about keys
371 */
372 unbindHandlers(handlerMap: Map<string, (data) => void>): void {
373 if (this.noHandlersWarn(handlerMap, 'unbindHandlers')) {
374 return null;
375 }
376
377 for (const [eventId, api] of handlerMap) {
378 this.handlers.delete(eventId);
379 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100380 }
381
Sean Condonfd6d11b2018-06-02 20:29:49 +0100382 /**
383 * Add a listener function for listening for WebSocket opening.
384 * The function must give a host and url and return void
385 */
386 addOpenListener(callback: (host: string, url: string) => void ): Callback {
387 const id: number = this.nextListenerId++;
388 const cb = this.fs.isF(callback);
389 const o: Callback = <Callback>{ id: id, cb: cb };
390
391 if (cb) {
392 this.openListeners.set(id, o);
393 } else {
394 this.log.error('WSS.addOpenListener(): callback not a function');
395 o.error = 'No callback defined';
396 }
397 return o;
Sean Condon83fc39f2018-04-19 18:56:13 +0100398 }
399
Sean Condonfd6d11b2018-06-02 20:29:49 +0100400 /**
401 * Remove a listener of WebSocket opening
402 */
403 removeOpenListener(lsnr: Callback): void {
404 const id = this.fs.isO(lsnr) && lsnr.id;
405 let o;
406
407 if (!id) {
408 this.log.warn('WSS.removeOpenListener(): invalid listener', lsnr);
409 return null;
410 }
411 o = this.openListeners[id];
412
413 if (o) {
414 this.openListeners.delete(id);
415 }
416 }
417
418 /**
419 * Formulates an event message and sends it via the web-socket.
Sean Condon83fc39f2018-04-19 18:56:13 +0100420 * If the websocket is not up yet, we store it in a pending list.
421 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100422 sendEvent(evType: string, payload: Object ): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100423 const ev = <EventType> {
Sean Condon83fc39f2018-04-19 18:56:13 +0100424 event: evType,
425 payload: payload
Sean Condon49e15be2018-05-16 16:58:29 +0100426 };
Sean Condon83fc39f2018-04-19 18:56:13 +0100427
428 if (this.wsUp) {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100429 this.send(ev);
Sean Condon83fc39f2018-04-19 18:56:13 +0100430 } else {
431 this.pendingEvents.push(ev);
432 }
433 }
434
Sean Condonfd6d11b2018-06-02 20:29:49 +0100435 /**
436 * Binds the veil service as a delegate.
437 */
438 setVeilDelegate(vd: VeilComponent): void {
439 this.vcd = vd;
440 }
441
442 /**
443 * Binds the loading service as a delegate
444 */
445 setLoadingDelegate(ld: any): void {
446 // TODO - Investigate changing Loading Service to LoadingComponent
447 this.log.debug('Loading delegate set', ld);
448 this.lcd = ld;
Sean Condon83fc39f2018-04-19 18:56:13 +0100449 }
450
451}