diff --git a/package.json b/package.json index 219ce1a..c4b9457 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json", - "build": "ng build --prod --aot --build-optimizer" + "build": "ng build --prod --aot --build-optimizer", + "lint": "ng lint --fix" }, "private": true, "dependencies": { diff --git a/src/app/services/terminado.service.ts b/src/app/services/terminado.service.ts index eb523a8..275c334 100644 --- a/src/app/services/terminado.service.ts +++ b/src/app/services/terminado.service.ts @@ -8,11 +8,16 @@ export class TerminadoService { xterm: Terminal; ws: WebSocket; attached = false; + private dataListener: any; + private resizeListener: any; constructor() { Terminal.applyAddon(fit); - Terminal.applyAddon(terminado); - this.xterm = new Terminal({ + this.xterm = this.createTerminal(); + } + + createTerminal() { + return new Terminal({ theme: { foreground: '#ffffff', background: '#0C0C0C', // $tao-gray-800 @@ -37,26 +42,52 @@ export class TerminadoService { }, fontSize: 14 }); - - const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - this.ws = new WebSocket(wsproto + window.location.host + '/terminal'); } attach(element: HTMLElement) { + if (this.attached) { + return; + } + + const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://'; + this.ws = new WebSocket(wsproto + window.location.host + '/terminal'); + this.ws.onopen = () => { - (this.xterm).terminadoAttach(this.ws); + this.attached = true; + this.xterm = this.createTerminal(); this.xterm.open(element); this.fit(); - this.xterm.blur(); - this.attached = true; + // In order to reset the terminal state after a broken socket, we need to register the listeners manually. + (this.xterm)._core.register(this.dataListener = this.xterm.onData(data => { + this.ws.send(JSON.stringify(['stdin', data])); + })); + (this.xterm)._core.register(this.resizeListener = this.xterm.onResize((size: { rows: number, cols: number }) => { + this.ws.send(JSON.stringify(['set_size', size.rows, size.cols])); + })); + }; + + this.ws.onclose = () => { + this.detach(); + this.xterm.destroy(); + this.attach(element); + }; + + this.ws.onmessage = msg => { + const data = JSON.parse(msg.data); + if (data[0] === 'stdout') { + this.xterm.write(data[1]); + } }; } detach() { - (this.xterm).terminadoDetach(this.ws); - this.xterm.destroy(); - this.ws.close(); + if (!this.attached) { + return; + } + this.attached = false; + this.dataListener.dispose(); + this.resizeListener.dispose(); } fit() { diff --git a/src/app/services/websocket.service.ts b/src/app/services/websocket.service.ts index 353d2cc..4c85777 100644 --- a/src/app/services/websocket.service.ts +++ b/src/app/services/websocket.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, Subject, Subscription } from 'rxjs'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { filter, map } from 'rxjs/operators'; import { WebSocketMessage } from '../message-types/websocket-message'; @@ -17,14 +17,28 @@ export enum Intent { @Injectable() export class WebSocketService { private ws: WebSocketSubject; + private subject: Subject = new Subject(); + private subscription: Subscription; constructor() {} public connect() { if (!this.ws) { + if (this.subscription) { + this.subscription.unsubscribe(); + } const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://'; const connAddr = wsproto + window.location.host + '/ws'; - this.ws = webSocket(connAddr); + this.ws = webSocket({ + url: connAddr, + closeObserver: { + next: closeEvent => { + this.ws = null; + this.connect(); + } + } + }); + this.subscription = this.ws.subscribe(msg => this.subject.next(msg)); } } @@ -35,7 +49,7 @@ export class WebSocketService { } public observeAll(key: string): Observable { - return this.ws.pipe( + return this.subject.pipe( filter(message => message.key.startsWith(key)), map(message => message) );