c4f2787a37febb6c7c83261552cca42e06ac026e
[src/xds/xds-agent.git] / webapp / src / app / services / xdsagent.service.ts
1 import { Injectable } from '@angular/core';
2 import { Http, Headers, RequestOptionsArgs, Response } from '@angular/http';
3 import { Location } from '@angular/common';
4 import { Observable } from 'rxjs/Observable';
5 import { Subject } from 'rxjs/Subject';
6 import { BehaviorSubject } from 'rxjs/BehaviorSubject';
7 import * as io from 'socket.io-client';
8
9 import { AlertService } from './alert.service';
10 import { ISdk } from './sdk.service';
11 import { ProjectType} from "./project.service";
12
13 // Import RxJs required methods
14 import 'rxjs/add/operator/map';
15 import 'rxjs/add/operator/catch';
16 import 'rxjs/add/observable/throw';
17 import 'rxjs/add/operator/mergeMap';
18 import 'rxjs/add/observable/of';
19 import 'rxjs/add/operator/retryWhen';
20
21
22 export interface IXDSConfigProject {
23     id: string;
24     path: string;
25     clientSyncThingID: string;
26     type: string;
27     label?: string;
28     defaultSdkID?: string;
29 }
30
31 interface IXDSBuilderConfig {
32     ip: string;
33     port: string;
34     syncThingID: string;
35 }
36
37 export interface IXDSProjectConfig {
38     id: string;
39     serverId: string;
40     label: string;
41     clientPath: string;
42     serverPath?: string;
43     type: ProjectType;
44     status?: string;
45     isInSync?: boolean;
46     defaultSdkID: string;
47 }
48
49 export interface IXDSVer {
50     id: string;
51     version: string;
52     apiVersion: string;
53     gitTag: string;
54 }
55
56 export interface IXDSVersions {
57     client: IXDSVer;
58     servers: IXDSVer[];
59 }
60
61 export interface IXDServerCfg {
62     id: string;
63     url: string;
64     apiUrl: string;
65     partialUrl: string;
66     connRetry: number;
67     connected: boolean;
68 }
69
70 export interface IXDSConfig {
71     servers: IXDServerCfg[];
72 }
73
74 export interface ISdkMessage {
75     wsID: string;
76     msgType: string;
77     data: any;
78 }
79
80 export interface ICmdOutput {
81     cmdID: string;
82     timestamp: string;
83     stdout: string;
84     stderr: string;
85 }
86
87 export interface ICmdExit {
88     cmdID: string;
89     timestamp: string;
90     code: number;
91     error: string;
92 }
93
94 export interface IAgentStatus {
95     WS_connected: boolean;
96 }
97
98
99 @Injectable()
100 export class XDSAgentService {
101
102     public XdsConfig$: Observable<IXDSConfig>;
103     public Status$: Observable<IAgentStatus>;
104     public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject();
105     public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
106     public CmdExit$ = <Subject<ICmdExit>>new Subject();
107
108     private baseUrl: string;
109     private wsUrl: string;
110     private _config = <IXDSConfig>{ servers: [] };
111     private _status = { WS_connected: false };
112
113     private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config);
114     private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status);
115
116     private socket: SocketIOClient.Socket;
117
118     constructor(private http: Http, private _window: Window, private alert: AlertService) {
119
120         this.XdsConfig$ = this.configSubject.asObservable();
121         this.Status$ = this.statusSubject.asObservable();
122
123         this.baseUrl = this._window.location.origin + '/api/v1';
124
125         let re = this._window.location.origin.match(/http[s]?:\/\/([^\/]*)[\/]?/);
126         if (re === null || re.length < 2) {
127             console.error('ERROR: cannot determine Websocket url');
128         } else {
129             this.wsUrl = 'ws://' + re[1];
130             this._handleIoSocket();
131             this._RegisterEvents();
132         }
133     }
134
135     private _WSState(sts: boolean) {
136         this._status.WS_connected = sts;
137         this.statusSubject.next(Object.assign({}, this._status));
138
139         // Update XDS config including XDS Server list when connected
140         if (sts) {
141             this.getConfig().subscribe(c => {
142                 this._config = c;
143                 this.configSubject.next(
144                     Object.assign({ servers: [] }, this._config)
145                 );
146             });
147         }
148     }
149
150     private _handleIoSocket() {
151         this.socket = io(this.wsUrl, { transports: ['websocket'] });
152
153         this.socket.on('connect_error', (res) => {
154             this._WSState(false);
155             console.error('XDS Agent WebSocket Connection error !');
156         });
157
158         this.socket.on('connect', (res) => {
159             this._WSState(true);
160         });
161
162         this.socket.on('disconnection', (res) => {
163             this._WSState(false);
164             this.alert.error('WS disconnection: ' + res);
165         });
166
167         this.socket.on('error', (err) => {
168             console.error('WS error:', err);
169         });
170
171         this.socket.on('make:output', data => {
172             this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
173         });
174
175         this.socket.on('make:exit', data => {
176             this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
177         });
178
179         this.socket.on('exec:output', data => {
180             this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data));
181         });
182
183         this.socket.on('exec:exit', data => {
184             this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
185         });
186
187         // Events
188         // (project-add and project-delete events are managed by project.service)
189         this.socket.on('event:server-config', ev => {
190             if (ev && ev.data) {
191                 let cfg: IXDServerCfg = ev.data;
192                 let idx = this._config.servers.findIndex(el => el.id === cfg.id);
193                 if (idx >= 0) {
194                     this._config.servers[idx] = Object.assign({}, cfg);
195                 }
196                 this.configSubject.next(Object.assign({}, this._config));
197             }
198         });
199
200         this.socket.on('event:project-state-change', ev => {
201             if (ev && ev.data) {
202                 this.ProjectState$.next(Object.assign({}, ev.data));
203             }
204         });
205
206     }
207
208     /**
209     ** Events
210     ***/
211     addEventListener(ev: string, fn: Function): SocketIOClient.Emitter {
212         return this.socket.addEventListener(ev, fn);
213     }
214
215     /**
216     ** Misc / Version
217     ***/
218     getVersion(): Observable<IXDSVersions> {
219         return this._get('/version');
220     }
221
222     /***
223     ** Config
224     ***/
225     getConfig(): Observable<IXDSConfig> {
226         return this._get('/config');
227     }
228
229     setConfig(cfg: IXDSConfig): Observable<IXDSConfig> {
230         return this._post('/config', cfg);
231     }
232
233     setServerRetry(serverID: string, r: number) {
234         let svr = this._getServer(serverID);
235         if (!svr) {
236             return Observable.of([]);
237         }
238
239         svr.connRetry = r;
240         this.setConfig(this._config).subscribe(
241             newCfg => {
242                 this._config = newCfg;
243                 this.configSubject.next(Object.assign({}, this._config));
244             },
245             err => {
246                 this.alert.error(err);
247             }
248         );
249     }
250
251     setServerUrl(serverID: string, url: string) {
252         let svr = this._getServer(serverID);
253         if (!svr) {
254             return Observable.of([]);
255         }
256         svr.url = url;
257         this.setConfig(this._config).subscribe(
258             newCfg => {
259                 this._config = newCfg;
260                 this.configSubject.next(Object.assign({}, this._config));
261             },
262             err => {
263                 this.alert.error(err);
264             }
265         );
266     }
267
268     /***
269     ** SDKs
270     ***/
271     getSdks(serverID: string): Observable<ISdk[]> {
272         let svr = this._getServer(serverID);
273         if (!svr || !svr.connected) {
274             return Observable.of([]);
275         }
276
277         return this._get(svr.partialUrl + '/sdks');
278     }
279
280     /***
281     ** Projects
282     ***/
283     getProjects(): Observable<IXDSProjectConfig[]> {
284         return this._get('/projects');
285     }
286
287     addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> {
288         return this._post('/projects', cfg);
289     }
290
291     deleteProject(id: string): Observable<IXDSProjectConfig> {
292         return this._delete('/projects/' + id);
293     }
294
295     syncProject(id: string): Observable<string> {
296         return this._post('/projects/sync/' + id, {});
297     }
298
299     /***
300     ** Exec
301     ***/
302     exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
303         return this._post('/exec',
304             {
305                 id: prjID,
306                 rpath: dir,
307                 cmd: cmd,
308                 sdkID: sdkid || "",
309                 args: args || [],
310                 env: env || [],
311             });
312     }
313
314     make(prjID: string, dir: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
315         // SEB TODO add serverID
316         return this._post('/make',
317             {
318                 id: prjID,
319                 rpath: dir,
320                 sdkID: sdkid,
321                 args: args || [],
322                 env: env || [],
323             });
324     }
325
326
327     /**
328     ** Private functions
329     ***/
330
331     private _RegisterEvents() {
332         // Register to all existing events
333         this._post('/events/register', { "name": "event:all" })
334             .subscribe(
335             res => { },
336             error => {
337                 this.alert.error("ERROR while registering to all events: ", error);
338             }
339             );
340     }
341
342     private _getServer(ID: string): IXDServerCfg {
343         let svr = this._config.servers.filter(item => item.id === ID);
344         if (svr.length < 1) {
345             return null;
346         }
347         return svr[0];
348     }
349
350     private _attachAuthHeaders(options?: any) {
351         options = options || {};
352         let headers = options.headers || new Headers();
353         // headers.append('Authorization', 'Basic ' + btoa('username:password'));
354         headers.append('Accept', 'application/json');
355         headers.append('Content-Type', 'application/json');
356         // headers.append('Access-Control-Allow-Origin', '*');
357
358         options.headers = headers;
359         return options;
360     }
361
362     private _get(url: string): Observable<any> {
363         return this.http.get(this.baseUrl + url, this._attachAuthHeaders())
364             .map((res: Response) => res.json())
365             .catch(this._decodeError);
366     }
367     private _post(url: string, body: any): Observable<any> {
368         return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders())
369             .map((res: Response) => res.json())
370             .catch((error) => {
371                 return this._decodeError(error);
372             });
373     }
374     private _delete(url: string): Observable<any> {
375         return this.http.delete(this.baseUrl + url, this._attachAuthHeaders())
376             .map((res: Response) => res.json())
377             .catch(this._decodeError);
378     }
379
380     private _decodeError(err: any) {
381         let e: string;
382         if (err instanceof Response) {
383             const body = err.json() || 'Agent error';
384             e = body.error || JSON.stringify(body);
385             if (!e || e === "" || e === '{"isTrusted":true}') {
386                 e = `${err.status} - ${err.statusText || 'Unknown error'}`;
387             }
388         } else if (typeof err === "object") {
389             if (err.statusText) {
390                 e = err.statusText;
391             } else if (err.error) {
392                 e = String(err.error);
393             } else {
394                 e = JSON.stringify(err);
395             }
396         } else {
397             e = err.message ? err.message : err.toString();
398         }
399         return Observable.throw(e);
400     }
401 }