/** * @license * Copyright (C) 2018 "IoT.bzh" * Author Sebastien Douheret * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Injectable, SecurityContext, isDevMode } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { XDSAgentService, IXDSTargetConfig, IXDSTargetTerminal } from '../services/xdsagent.service'; /* FIXME: syntax only compatible with TS>2.4.0 export enum TargetTypeEnum { UNSET = '', STANDARD: 'standard', } */ export type TargetTypeEnum = '' | 'standard'; export const TargetType = { UNSET: '', STANDARD: 'standard', }; export const TargetTypes = [ { value: TargetType.STANDARD, display: 'Standard' }, ]; export const TargetStatus = { ErrorConfig: 'ErrorConfig', Disable: 'Disable', Enable: 'Enable', }; export type TerminalTypeEnum = '' | 'ssh'; export const TerminalType = { UNSET: '', SSH: 'ssh', }; export interface ITarget extends IXDSTargetConfig { isUsable?: boolean; } export interface ITerminal extends IXDSTargetTerminal { targetID?: string; } export interface ITerminalOutput { termID: string; timestamp: string; stdout: string; stderr: string; } export interface ITerminalExit { termID: string; timestamp: string; code: number; error: string; } @Injectable() export class TargetService { public targets$: Observable; public curTarget$: Observable; public terminalOutput$ = >new Subject(); public terminalExit$ = >new Subject(); private _tgtsList: ITarget[] = []; private tgtsSubject = >new BehaviorSubject(this._tgtsList); private _current: ITarget; private curTgtSubject = >new BehaviorSubject(this._current); private curServerID; private termSocket: SocketIOClient.Socket; constructor(private xdsSvr: XDSAgentService) { this._current = null; this.targets$ = this.tgtsSubject.asObservable(); this.curTarget$ = this.curTgtSubject.asObservable(); this.xdsSvr.XdsConfig$.subscribe(cfg => { if (!cfg || cfg.servers.length < 1) { return; } // FIXME support multiple server this.curServerID = cfg.servers[0].id; // Load initial targets list this.xdsSvr.getTargets(this.curServerID).subscribe((targets) => { this._tgtsList = []; targets.forEach(p => { this._addTarget(p, true); }); // TODO: get previous val from xds-config service / cookie if (this._tgtsList.length > 0) { this._current = this._tgtsList[0]; this.curTgtSubject.next(this._current); } this.tgtsSubject.next(this._tgtsList); }); }); // Add listener on targets creation, deletion and change events this.xdsSvr.onTargetAdd().subscribe(tgt => this._addTarget(tgt)); this.xdsSvr.onTargetDelete().subscribe(tgt => this._delTarget(tgt)); this.xdsSvr.onTargetChange().subscribe(tgt => this._updateTarget(tgt)); // Register events to forward terminal Output and Exit this.xdsSvr.onSocketConnect().subscribe(socket => { this.termSocket = socket; // Handle terminal output socket.on('term:output', data => { const termOut = { termID: data.termID, timestamp: data.timestamp, stdout: atob(data.stdout), stderr: atob(data.stderr), }; this.terminalOutput$.next(termOut); }); // Handle terminal exit event socket.on('term:exit', data => { this.terminalExit$.next(Object.assign({}, data)); }); }); } setCurrent(p: ITarget): ITarget | undefined { if (!p) { this._current = null; return undefined; } return this.setCurrentById(p.id); } setCurrentById(id: string): ITarget | undefined { const p = this._tgtsList.find(item => item.id === id); if (p) { this._current = p; this.curTgtSubject.next(this._current); } return this._current; } getCurrent(): ITarget { return this._current; } getTargetById(id: string): ITarget | undefined { const t = this._tgtsList.find(item => item.id === id); return t; } add(tgt: ITarget): Observable { return this.xdsSvr.addTarget(this.curServerID, tgt); } delete(tgt: ITarget): Observable { const idx = this._getTargetIdx(tgt.id); const delTgt = tgt; if (idx === -1) { throw new Error('Invalid target id (id=' + tgt.id + ')'); } return this.xdsSvr.deleteTarget(this.curServerID, tgt.id) .map(res => delTgt); } setSettings(tgt: ITarget): Observable { return this.xdsSvr.updateTarget(this.curServerID, tgt); } terminalOpen(tgtID: string, termID: string, cfg?: IXDSTargetTerminal): Observable { if (termID === '' || termID === undefined) { // create a new terminal when no termID is set if (cfg === undefined) { cfg = { name: 'ssh to ' + this.getTargetById(tgtID).name, type: TerminalType.SSH, }; } return this.xdsSvr.createTerminalTarget(this.curServerID, tgtID, cfg) .flatMap(res => { return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, res.id); }); } else { return this.xdsSvr.openTerminalTarget(this.curServerID, tgtID, termID); } } terminalClose(tgtID, termID: string): Observable { return this.xdsSvr.closeTerminalTarget(this.curServerID, tgtID, termID); } terminalWrite(data: string) { if (this.termSocket) { this.termSocket.emit('term:input', btoa(data)); } } terminalResize(tgtID, termID: string, cols, rows: number): Observable { return this.xdsSvr.resizeTerminalTarget(this.curServerID, tgtID, termID, cols, rows); } /*** Private functions ***/ private _isUsableTarget(p) { return p && (p.status === TargetStatus.Enable); } private _getTargetIdx(id: string): number { return this._tgtsList.findIndex((item) => item.id === id); } private _addTarget(tgt: ITarget, noNext?: boolean): ITarget { tgt.isUsable = this._isUsableTarget(tgt); // add new target this._tgtsList.push(tgt); // sort target array this._tgtsList.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; }); if (!noNext) { this.tgtsSubject.next(this._tgtsList); } return tgt; } private _delTarget(tgt: ITarget) { const idx = this._tgtsList.findIndex(item => item.id === tgt.id); if (idx === -1) { if (isDevMode) { /* tslint:disable:no-console */ console.log('Warning: Try to delete target unknown id: tgt=', tgt); } return; } const delId = this._tgtsList[idx].id; this._tgtsList.splice(idx, 1); if (delId === this._current.id) { this.setCurrent(this._tgtsList[0]); } this.tgtsSubject.next(this._tgtsList); } private _updateTarget(tgt: ITarget) { const i = this._getTargetIdx(tgt.id); if (i >= 0) { this._tgtsList[i].status = tgt.status; this._tgtsList[i].isUsable = this._isUsableTarget(tgt); this.tgtsSubject.next(this._tgtsList); } } }