@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
236 lines (230 loc) • 11.5 kB
JavaScript
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