UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

236 lines (230 loc) 11.5 kB
import * as i0 from '@angular/core'; import { ViewEncapsulation, Component } from '@angular/core'; import { TitleComponent, C8yTranslatePipe, ActionBarItemComponent, IconDirective } from '@c8y/ngx-components'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import * as i1 from '@c8y/ngx-components/remote-access/data'; import * as i2 from '@angular/router'; import '@xterm/xterm/css/xterm.css'; import { NgIf } from '@angular/common'; class TelnetNegotiator { constructor(terminal, termType) { this.terminal = terminal; this.termType = termType; } setTermType(type) { this.termType = type; } pushStr(str, arr) { for (let i = 0; i < str.length; i++) { arr.push(str.charCodeAt(i)); } } negotiate(data) { const arrUint8 = new Uint8Array(data); const arr = [...arrUint8.values()]; const sendQueue = []; let stringToDisplay = ''; let chr; let code; let value; while (arr.length > 0) { chr = arr.shift(); switch (chr) { case 255: // IAC code = arr.shift(); value = arr.shift(); switch (code) { case 253: // DO if (value === 24) { // Terminal type sendQueue.push(255, 251, value); } else if (value === 31) { // NAWS (Negotiate About Window Size) // client answers WILL NAWS sendQueue.push(255, 251, 31); // client sends IAC SB NAWS 0 terminalSize.cols 0 terminalSize.rows IAC SE sendQueue.push(255, 250, 31, 0, this.terminal.cols, 0, this.terminal.rows, 255, 240); } else { // Refuse other DO requests with a WONT sendQueue.push(255, 252, value); } break; case 251: // WILL if (value === 1) { // Affirm echo with DO sendQueue.push(255, 253, value); } else { // Reject other WILL offers with a DONT sendQueue.push(255, 254, value); } break; case 250: // SB (subnegotiation) if (value === 24) { // TERM-TYPE subnegotiation if (arr[0] === 1 && arr[1] === 255 && arr[2] === 240) { arr.shift(); arr.shift(); arr.shift(); sendQueue.push(255, 250, 24, 0); this.pushStr(this.termType, sendQueue); sendQueue.push(255, 240); } else { console.warn('Invalid subnegotiation received' + arr); } } else { console.warn('Ignoring SB ' + value); } break; case 254: // DONT case 252: // WONT default: break; } break; case 242: // Data Mark (Synch) code = arr.shift(); value = arr.shift(); break; default: // everything else stringToDisplay += String.fromCharCode(chr); break; } } return { dataToSend: sendQueue.length ? new Uint8Array(sendQueue) : '', isOutput: !!stringToDisplay.length }; } } class ShellAdapter { constructor(terminal, termType = 'xterm-256color') { this.terminal = terminal; this.termType = termType; } filterNonPrintable(str) { // get rid of � const helpArr = str.split('�'); str = helpArr.join(''); return str; } setTermType(type) { this.termType = type; } filterReceiveData(data) { // negotiate telnet signals, if any const telnet = new TelnetNegotiator(this.terminal, this.termType); const telnetData = telnet.negotiate(data); // decode text for display const textDecoder = new TextDecoder(); const decodedToDisplay = textDecoder.decode(data); return { dataToSend: telnetData.dataToSend, dataToDisplay: telnetData.isOutput ? this.filterNonPrintable(decodedToDisplay) : '' }; } filterSendData(data) { const sendQueue = []; for (let i = 0; i < data.length; i++) { sendQueue.push(data.charCodeAt(i)); } return sendQueue.length ? new Uint8Array(sendQueue) : ''; } } class TerminalViewerComponent { constructor(remoteAccess, activatedRoute) { this.remoteAccess = remoteAccess; this.activatedRoute = activatedRoute; this.title = ''; this.container = null; this.terminal = null; this.socket = null; this.firstDeviceMessageReceived = false; this.configurationId = this.activatedRoute.snapshot.params.configurationId; this.deviceId = this.activatedRoute.parent.snapshot.params.id; } ngOnDestroy() { this.observer?.disconnect(); const stringToSend = 'exit\n'; const sendQueue = []; for (let i = 0; i < stringToSend.length; i++) { sendQueue.push(stringToSend.charCodeAt(i)); } this.socket?.send(new Uint8Array(sendQueue)); this.socket?.close(); } ngAfterViewInit() { this.container = document.getElementById('terminal-screen'); this.terminal = new Terminal({ fontSize: 18, fontFamily: 'consolas, monospace', cursorBlink: true }); const fitAddon = new FitAddon(); this.socket = new WebSocket(this.remoteAccess.getWebSocketUri(this.deviceId, this.configurationId), 'binary'); this.socket.binaryType = 'arraybuffer'; this.terminal.loadAddon(fitAddon); this.terminal.open(this.container); fitAddon.fit(); const shellAdapter = new ShellAdapter(this.terminal); this.observer = new ResizeObserver(() => { fitAddon.fit(); }); this.observer.observe(this.container); this.terminal.writeln('\u001b[92mEstablishing Websocket connection... \u001b[39m \r\n'); this.terminal.focus(); this.socket.onopen = () => { this.terminal.writeln('\u001b[92mWebsocket connection was established successfully, waiting for device... \u001b[39m \r\n'); }; this.socket.onclose = () => { this.terminal.writeln(''); this.terminal.writeln('\r\n\u001b[91mDevice disconnected. \u001b[39m \r\n'); this.terminal.writeln('\r\n\u001b[91mWebsocket connection was closed. \u001b[39m \r\n'); }; this.socket.onmessage = message => { if (!this.firstDeviceMessageReceived) { this.firstDeviceMessageReceived = true; this.terminal.writeln('\u001b[92mDevice connection was established successfully. \u001b[39m \r\n'); } const filteredData = shellAdapter.filterReceiveData(message.data); if (filteredData.dataToSend.length) { this.socket.send(filteredData.dataToSend); } if (filteredData.dataToDisplay.length) { this.terminal.write(filteredData.dataToDisplay); } }; this.terminal.onData(data => { if (this.firstDeviceMessageReceived) { const encodedToSend = shellAdapter.filterSendData(data); if (encodedToSend.length) { this.socket.send(encodedToSend); } } }); } toggleFullscreen() { if (document.fullscreenElement) { document.exitFullscreen(); } else { this.container.requestFullscreen(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TerminalViewerComponent, deps: [{ token: i1.RemoteAccessService }, { token: i2.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: TerminalViewerComponent, isStandalone: true, selector: "c8y-terminal-viewer", ngImport: i0, template: "<c8y-title>Terminal Viewer: {{ title | translate }}</c8y-title>\n\n<c8y-action-bar-item [placement]=\"'right'\" *ngIf=\"firstDeviceMessageReceived\">\n <button\n class=\"btn btn-link\"\n (click)=\"toggleFullscreen()\"\n >\n <i [c8yIcon]=\"'expand'\"></i>\n <span translate>Fullscreen</span>\n </button>\n </c8y-action-bar-item>\n\n<div class=\"content-fullpage\" >\n <div id=\"terminal-screen\" class=\"inner-scroll\"></div>\n</div>", dependencies: [{ kind: "component", type: TitleComponent, selector: "c8y-title", inputs: ["pageTitleUpdate"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TerminalViewerComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-terminal-viewer', encapsulation: ViewEncapsulation.None, standalone: true, imports: [TitleComponent, C8yTranslatePipe, ActionBarItemComponent, IconDirective, NgIf], template: "<c8y-title>Terminal Viewer: {{ title | translate }}</c8y-title>\n\n<c8y-action-bar-item [placement]=\"'right'\" *ngIf=\"firstDeviceMessageReceived\">\n <button\n class=\"btn btn-link\"\n (click)=\"toggleFullscreen()\"\n >\n <i [c8yIcon]=\"'expand'\"></i>\n <span translate>Fullscreen</span>\n </button>\n </c8y-action-bar-item>\n\n<div class=\"content-fullpage\" >\n <div id=\"terminal-screen\" class=\"inner-scroll\"></div>\n</div>" }] }], ctorParameters: () => [{ type: i1.RemoteAccessService }, { type: i2.ActivatedRoute }] }); /** * Generated bundle index. Do not edit. */ export { TerminalViewerComponent }; //# sourceMappingURL=c8y-ngx-components-remote-access-terminal-viewer.mjs.map