Reworked SDKs events (introduced sdk-state-change)
[src/xds/xds-agent.git] / webapp / src / app / @core-xds / services / xdsagent.service.ts
1 /**
2 * @license
3 * Copyright (C) 2017-2018 "IoT.bzh"
4 * Author Sebastien Douheret <sebastien@iot.bzh>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19 import { Injectable, Inject, isDevMode } from '@angular/core';
20 import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
21 import { DOCUMENT } from '@angular/common';
22 import { Observable } from 'rxjs/Observable';
23 import { Subject } from 'rxjs/Subject';
24 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
25 import * as io from 'socket.io-client';
26
27 import { AlertService } from './alert.service';
28 import { ISdk, ISdkManagementMsg } from './sdk.service';
29 import { ProjectType, ProjectTypeEnum } from './project.service';
30
31 // Import RxJs required methods
32 import 'rxjs/add/operator/map';
33 import 'rxjs/add/operator/catch';
34 import 'rxjs/add/observable/throw';
35 import 'rxjs/add/operator/mergeMap';
36 import 'rxjs/add/observable/of';
37 import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
38
39
40 export interface IXDSConfigProject {
41   id: string;
42   path: string;
43   clientSyncThingID: string;
44   type: string;
45   label?: string;
46   defaultSdkID?: string;
47 }
48
49 interface IXDSBuilderConfig {
50   ip: string;
51   port: string;
52   syncThingID: string;
53 }
54
55 export interface IXDSProjectConfig {
56   id: string;
57   serverId: string;
58   label: string;
59   clientPath: string;
60   serverPath?: string;
61   type: ProjectTypeEnum;
62   status?: string;
63   isInSync?: boolean;
64   defaultSdkID: string;
65   clientData?: string;
66 }
67
68 export interface IXDSVer {
69   id: string;
70   version: string;
71   apiVersion: string;
72   gitTag: string;
73 }
74
75 export interface IXDSVersions {
76   client: IXDSVer;
77   servers: IXDSVer[];
78 }
79
80 export interface IXDServerCfg {
81   id: string;
82   url: string;
83   apiUrl?: string;
84   partialUrl?: string;
85   connRetry: number;
86   connected: boolean;
87 }
88
89 export interface IXDSConfig {
90   servers: IXDServerCfg[];
91 }
92
93 export interface ISdkMessage {
94   wsID: string;
95   msgType: string;
96   data: any;
97 }
98
99 export interface ICmdOutput {
100   cmdID: string;
101   timestamp: string;
102   stdout: string;
103   stderr: string;
104 }
105
106 export interface ICmdExit {
107   cmdID: string;
108   timestamp: string;
109   code: number;
110   error: string;
111 }
112
113 export interface IServerStatus {
114   id: string;
115   connected: boolean;
116 }
117
118 export interface IAgentStatus {
119   connected: boolean;
120   servers: IServerStatus[];
121 }
122
123
124 @Injectable()
125 export class XDSAgentService {
126
127   public XdsConfig$: Observable<IXDSConfig>;
128   public Status$: Observable<IAgentStatus>;
129   public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
130   public CmdExit$ = <Subject<ICmdExit>>new Subject();
131
132   protected projectAdd$ = new Subject<IXDSProjectConfig>();
133   protected projectDel$ = new Subject<IXDSProjectConfig>();
134   protected projectChange$ = new Subject<IXDSProjectConfig>();
135
136   protected sdkAdd$ = new Subject<ISdk>();
137   protected sdkRemove$ = new Subject<ISdk>();
138   protected sdkChange$ = new Subject<ISdk>();
139   protected sdkManagement$ = new Subject<ISdkManagementMsg>();
140
141   private baseUrl: string;
142   private wsUrl: string;
143   private httpSessionID: string;
144   private _config = <IXDSConfig>{ servers: [] };
145   private _status = { connected: false, servers: [] };
146
147   private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
148   private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
149
150   private socket: SocketIOClient.Socket;
151
152   constructor( @Inject(DOCUMENT) private document: Document,
153     private http: HttpClient, private alert: AlertService) {
154
155     this.XdsConfig$ = this.configSubject.asObservable();
156     this.Status$ = this.statusSubject.asObservable();
157
158     const originUrl = this.document.location.origin;
159     this.baseUrl = originUrl + '/api/v1';
160
161     // Retrieve Session ID / token
162     this.http.get(this.baseUrl + '/version', { observe: 'response' })
163       .subscribe(
164       resp => {
165         this.httpSessionID = resp.headers.get('xds-agent-sid');
166
167         const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/);
168         if (re === null || re.length < 2) {
169           console.error('ERROR: cannot determine Websocket url');
170         } else {
171           this.wsUrl = 'ws://' + re[1];
172           this._handleIoSocket();
173           this._RegisterEvents();
174         }
175       },
176       err => {
177         /* tslint:disable:no-console */
178         console.error('ERROR while retrieving session id:', err);
179       });
180   }
181
182   private _NotifyXdsAgentState(sts: boolean) {
183     this._status.connected = sts;
184     this.statusSubject.next(Object.assign({}, this._status));
185
186     // Update XDS config including XDS Server list when connected
187     if (sts) {
188       this.getConfig().subscribe(c => {
189         this._config = c;
190         this._NotifyXdsServerState();
191         this.configSubject.next(Object.assign({ servers: [] }, this._config));
192       });
193     }
194   }
195
196   private _NotifyXdsServerState() {
197     this._status.servers = this._config.servers.map(svr => {
198       return { id: svr.id, connected: svr.connected };
199     });
200     this.statusSubject.next(Object.assign({}, this._status));
201   }
202
203   private _handleIoSocket() {
204     this.socket = io(this.wsUrl, { transports: ['websocket'] });
205
206     this.socket.on('connect_error', (res) => {
207       this._NotifyXdsAgentState(false);
208       console.error('XDS Agent WebSocket Connection error !');
209     });
210
211     this.socket.on('connect', (res) => {
212       this._NotifyXdsAgentState(true);
213     });
214
215     this.socket.on('disconnection', (res) => {
216       this._NotifyXdsAgentState(false);
217       this.alert.error('WS disconnection: ' + res);
218     });
219
220     this.socket.on('error', (err) => {
221       console.error('WS error:', err);
222     });
223
224     // XDS Events decoding
225
226     this.socket.on('make:output', data => {
227       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
228     });
229
230     this.socket.on('make:exit', data => {
231       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
232     });
233
234     this.socket.on('exec:output', data => {
235       this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
236     });
237
238     this.socket.on('exec:exit', data => {
239       this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
240     });
241
242     this.socket.on('event:server-config', ev => {
243       if (ev && ev.data) {
244         const cfg: IXDServerCfg = ev.data;
245         const idx = this._config.servers.findIndex(el => el.id === cfg.id);
246         if (idx >= 0) {
247           this._config.servers[idx] = Object.assign({}, cfg);
248           this._NotifyXdsServerState();
249         }
250         this.configSubject.next(Object.assign({}, this._config));
251       }
252     });
253
254     /*** Project events ****/
255
256     this.socket.on('event:project-add', (ev) => {
257       if (ev && ev.data && ev.data.id) {
258         this.projectAdd$.next(Object.assign({}, ev.data));
259         if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) {
260           this.alert.info('Project "' + ev.data.label + '" has been added by another tool.');
261         }
262       } else if (isDevMode) {
263         /* tslint:disable:no-console */
264         console.log('Warning: received event:project-add with unknown data: ev=', ev);
265       }
266     });
267
268     this.socket.on('event:project-delete', (ev) => {
269       if (ev && ev.data && ev.data.id) {
270         this.projectDel$.next(Object.assign({}, ev.data));
271         if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && ev.data.label) {
272           this.alert.info('Project "' + ev.data.label + '" has been deleted by another tool.');
273         }
274       } else if (isDevMode) {
275         console.log('Warning: received event:project-delete with unknown data: ev=', ev);
276       }
277     });
278
279     this.socket.on('event:project-state-change', ev => {
280       if (ev && ev.data) {
281         this.projectChange$.next(Object.assign({}, ev.data));
282       } else if (isDevMode) {
283         console.log('Warning: received event:project-state-change with unkn220own data: ev=', ev);
284       }
285     });
286
287     /*** SDK Events ***/
288
289     this.socket.on('event:sdk-add', (ev) => {
290       if (ev && ev.data && ev.data.id) {
291         const evt = <ISdk>ev.data;
292         this.sdkAdd$.next(Object.assign({}, evt));
293
294         if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.name) {
295           this.alert.info('SDK "' + evt.name + '" has been added by another tool.');
296         }
297       } else if (isDevMode) {
298         console.log('Warning: received event:sdk-add with unknown data: ev=', ev);
299       }
300     });
301
302     this.socket.on('event:sdk-remove', (ev) => {
303       if (ev && ev.data && ev.data.id) {
304         const evt = <ISdk>ev.data;
305         this.sdkRemove$.next(Object.assign({}, evt));
306
307         if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.name) {
308           this.alert.info('SDK "' + evt.name + '" has been removed by another tool.');
309         }
310       } else if (isDevMode) {
311         console.log('Warning: received event:sdk-remove with unknown data: ev=', ev);
312       }
313     });
314
315     this.socket.on('event:sdk-state-change', (ev) => {
316       if (ev && ev.data && ev.data.id) {
317         const evt = <ISdk>ev.data;
318         this.sdkChange$.next(Object.assign({}, evt));
319
320       } else if (isDevMode) {
321         console.log('Warning: received event:sdk-state-change with unknown data: ev=', ev);
322       }
323     });
324
325
326     this.socket.on('event:sdk-management', (ev) => {
327       if (ev && ev.data && ev.data.sdk) {
328         const evt = <ISdkManagementMsg>ev.data;
329         this.sdkManagement$.next(Object.assign({}, evt));
330
331         if (ev.sessionID !== '' && ev.sessionID !== this.httpSessionID && evt.sdk.name) {
332           this.alert.info('SDK "' + evt.sdk.name + '" has been installed by another tool.');
333         }
334       } else if (isDevMode) {
335         /* tslint:disable:no-console */
336         console.log('Warning: received event:sdk-install with unknown data: ev=', ev);
337       }
338     });
339
340   }
341
342   /**
343   ** Events registration
344   ***/
345   onProjectAdd(): Observable<IXDSProjectConfig> {
346     return this.projectAdd$.asObservable();
347   }
348
349   onProjectDelete(): Observable<IXDSProjectConfig> {
350     return this.projectDel$.asObservable();
351   }
352
353   onProjectChange(): Observable<IXDSProjectConfig> {
354     return this.projectChange$.asObservable();
355   }
356
357   onSdkAdd(): Observable<ISdk> {
358     return this.sdkAdd$.asObservable();
359   }
360
361   onSdkRemove(): Observable<ISdk> {
362     return this.sdkRemove$.asObservable();
363   }
364
365   onSdkChange(): Observable<ISdk> {
366     return this.sdkChange$.asObservable();
367   }
368
369   onSdkManagement(): Observable<ISdkManagementMsg> {
370     return this.sdkManagement$.asObservable();
371   }
372
373   /**
374   ** Misc / Version
375   ***/
376   getVersion(): Observable<IXDSVersions> {
377     return this._get('/version');
378   }
379
380   /***
381   ** Config
382   ***/
383   getConfig(): Observable<IXDSConfig> {
384     return this._get('/config');
385   }
386
387   setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
388     return this._post('/config', cfg);
389   }
390
391   setServerRetry(serverID: string, retry: number): Observable<IXDSConfig> {
392     const svr = this._getServer(serverID);
393     if (!svr) {
394       return Observable.throw('Unknown server ID');
395     }
396     if (retry < 0 || Number.isNaN(retry) || retry == null) {
397       return Observable.throw('Not a valid number');
398     }
399     svr.connRetry = retry;
400     return this._setConfig();
401   }
402
403   setServerUrl(serverID: string, url: string, retry: number): Observable<IXDSConfig> {
404     const svr = this._getServer(serverID);
405     if (!svr) {
406       return Observable.throw('Unknown server ID');
407     }
408     svr.connected = false;
409     svr.url = url;
410     if (!Number.isNaN(retry) && retry > 0) {
411       svr.connRetry = retry;
412     }
413     this._NotifyXdsServerState();
414     return this._setConfig();
415   }
416
417   private _setConfig(): Observable<IXDSConfig> {
418     return this.setConfig(this._config)
419       .map(newCfg => {
420         this._config = newCfg;
421         this.configSubject.next(Object.assign({}, this._config));
422         return this._config;
423       });
424   }
425
426   /***
427   ** SDKs
428   ***/
429   getSdks(serverID: string): Observable<ISdk[]> {
430     const svr = this._getServer(serverID);
431     if (!svr || !svr.connected) {
432       return Observable.of([]);
433     }
434     return this._get(svr.partialUrl + '/sdks');
435   }
436
437   installSdk(serverID: string, id: string, filename?: string, force?: boolean): Observable<ISdk> {
438     return this._post(this._getServerUrl(serverID) + '/sdks', { id: id, filename: filename, force: force });
439   }
440
441   abortInstall(serverID: string, id: string): Observable<ISdk> {
442     return this._post(this._getServerUrl(serverID) + '/sdks/abortinstall', { id: id });
443   }
444
445   removeSdk(serverID: string, id: string): Observable<ISdk> {
446     return this._delete(this._getServerUrl(serverID) + '/sdks/' + id);
447   }
448
449
450   /***
451   ** Projects
452   ***/
453   getProjects(): Observable<IXDSProjectConfig[]> {
454     return this._get('/projects');
455   }
456
457   addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
458     return this._post('/projects', cfg);
459   }
460
461   deleteProject(id: string): Observable<IXDSProjectConfig> {
462     return this._delete('/projects/' + id);
463   }
464
465   updateProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
466     return this._put('/projects/' + cfg.id, cfg);
467   }
468
469   syncProject(id: string): Observable<string> {
470     return this._post('/projects/sync/' + id, {});
471   }
472
473   /***
474   ** Exec
475   ***/
476   exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
477     return this._post('/exec',
478       {
479         id: prjID,
480         rpath: dir,
481         cmd: cmd,
482         sdkID: sdkid || '',
483         args: args || [],
484         env: env || [],
485       });
486   }
487
488   /**
489   ** Private functions
490   ***/
491
492   private _RegisterEvents() {
493     // Register to all existing events
494     this._post('/events/register', { 'name': 'event:all' })
495       .subscribe(
496       res => { },
497       error => {
498         this.alert.error('ERROR while registering to all events: ' + error);
499       },
500     );
501   }
502
503   private _getServer(ID: string): IXDServerCfg {
504     const svr = this._config.servers.filter(item => item.id === ID);
505     if (svr.length < 1) {
506       return null;
507     }
508     return svr[0];
509   }
510
511   private _getServerUrl(serverID: string): string | ErrorObservable {
512     const svr = this._getServer(serverID);
513     if (!svr || !svr.connected) {
514       if (isDevMode) {
515         console.log('ERROR: XDS Server unknown: serverID=' + serverID);
516       }
517       return Observable.throw('Cannot identify XDS Server');
518     }
519     return svr.partialUrl;
520   }
521
522   private _attachAuthHeaders(options?: any) {
523     options = options || {};
524     const headers = options.headers || new HttpHeaders();
525     // headers.append('Authorization', 'Basic ' + btoa('username:password'));
526     headers.append('Accept', 'application/json');
527     headers.append('Content-Type', 'application/json');
528     // headers.append('Access-Control-Allow-Origin', '*');
529
530     options.headers = headers;
531     return options;
532   }
533
534   private _get(url: string): Observable<any> {
535     return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
536       .catch(this._decodeError);
537   }
538   private _post(url: string, body: any): Observable<any> {
539     return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
540       .catch((error) => {
541         return this._decodeError(error);
542       });
543   }
544   private _put(url: string, body: any): Observable<any> {
545     return this.http.put(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
546       .catch((error) => {
547         return this._decodeError(error);
548       });
549   }
550   private _delete(url: string): Observable<any> {
551     return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
552       .catch(this._decodeError);
553   }
554
555   private _decodeError(err: any) {
556     let e: string;
557     if (err instanceof HttpErrorResponse) {
558       e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error';
559     } else if (typeof err === 'object') {
560       if (err.statusText) {
561         e = err.statusText;
562       } else if (err.error) {
563         e = String(err.error);
564       } else {
565         e = JSON.stringify(err);
566       }
567     } else {
568       e = err.message ? err.message : err.toString();
569     }
570     /* tslint:disable:no-console */
571     if (isDevMode) {
572       console.log('xdsagent.service - ERROR: ', e);
573     }
574     return Observable.throw(e);
575   }
576 }