mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
173 lines
4.6 KiB
TypeScript
173 lines
4.6 KiB
TypeScript
import { stringify } from "querystring";
|
|
import { autobind, base64, EventEmitter, interval } from "../utils";
|
|
import { WebSocketApi } from "./websocket-api";
|
|
import { configStore } from "../config.store";
|
|
import isEqual from "lodash/isEqual"
|
|
|
|
export enum TerminalChannels {
|
|
STDIN = 0,
|
|
STDOUT = 1,
|
|
STDERR = 2,
|
|
TERMINAL_SIZE = 4,
|
|
TOKEN = 9,
|
|
}
|
|
|
|
enum TerminalColor {
|
|
RED = "\u001b[31m",
|
|
GREEN = "\u001b[32m",
|
|
YELLOW = "\u001b[33m",
|
|
BLUE = "\u001b[34m",
|
|
MAGENTA = "\u001b[35m",
|
|
CYAN = "\u001b[36m",
|
|
GRAY = "\u001b[90m",
|
|
LIGHT_GRAY = "\u001b[37m",
|
|
NO_COLOR = "\u001b[0m",
|
|
}
|
|
|
|
export interface ITerminalApiOptions {
|
|
id: string;
|
|
node?: string;
|
|
colorTheme?: "light" | "dark";
|
|
}
|
|
|
|
export class TerminalApi extends WebSocketApi {
|
|
protected size: { Width: number; Height: number };
|
|
protected currentToken: string;
|
|
protected tokenInterval = interval(60, this.sendNewToken); // refresh every minute
|
|
|
|
public onReady = new EventEmitter<[]>();
|
|
public isReady = false;
|
|
|
|
constructor(protected options: ITerminalApiOptions) {
|
|
super({
|
|
logging: configStore.isDevelopment,
|
|
flushOnOpen: false,
|
|
pingIntervalSeconds: 30,
|
|
});
|
|
}
|
|
|
|
async getUrl(token: string) {
|
|
const { hostname, protocol } = location;
|
|
const { id, node } = this.options;
|
|
const apiPrefix = configStore.apiPrefix.TERMINAL;
|
|
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
|
const queryParams = { token, id };
|
|
if (node) {
|
|
Object.assign(queryParams, {
|
|
node: node,
|
|
type: "node"
|
|
});
|
|
}
|
|
|
|
return `${wss}${hostname}${configStore.serverPort}${apiPrefix}/api?${stringify(queryParams)}`;
|
|
}
|
|
|
|
async connect() {
|
|
const token = await configStore.getToken();
|
|
const apiUrl = await this.getUrl(token);
|
|
const { colorTheme } = this.options;
|
|
this.emitStatus("Connecting...", {
|
|
color: colorTheme == "light" ? TerminalColor.GRAY : TerminalColor.LIGHT_GRAY
|
|
});
|
|
this.onData.addListener(this._onReady, { prepend: true });
|
|
this.currentToken = token;
|
|
this.tokenInterval.start();
|
|
return super.connect(apiUrl);
|
|
}
|
|
|
|
@autobind()
|
|
async sendNewToken() {
|
|
const token = await configStore.getToken();
|
|
if (!this.isReady || token == this.currentToken) return;
|
|
this.sendCommand(token, TerminalChannels.TOKEN);
|
|
this.currentToken = token;
|
|
}
|
|
|
|
destroy() {
|
|
if (!this.socket) return;
|
|
const exitCode = String.fromCharCode(4); // ctrl+d
|
|
this.sendCommand(exitCode);
|
|
this.tokenInterval.stop();
|
|
setTimeout(() => super.destroy(), 2000);
|
|
}
|
|
|
|
removeAllListeners() {
|
|
super.removeAllListeners();
|
|
this.onReady.removeAllListeners();
|
|
}
|
|
|
|
@autobind()
|
|
protected _onReady(data: string) {
|
|
if (!data) return;
|
|
this.isReady = true;
|
|
this.onReady.emit();
|
|
this.onData.removeListener(this._onReady);
|
|
this.flush();
|
|
this.onData.emit(data); // re-emit data
|
|
return false; // prevent calling rest of listeners
|
|
}
|
|
|
|
reconnect() {
|
|
const { reconnectDelaySeconds } = this.params;
|
|
if (reconnectDelaySeconds) {
|
|
this.emitStatus(`Reconnect in ${reconnectDelaySeconds} seconds`, {
|
|
color: TerminalColor.YELLOW,
|
|
showTime: true,
|
|
});
|
|
}
|
|
super.reconnect();
|
|
}
|
|
|
|
sendCommand(key: string, channel = TerminalChannels.STDIN) {
|
|
return this.send(channel + base64.encode(key));
|
|
}
|
|
|
|
sendTerminalSize(cols: number, rows: number) {
|
|
const newSize = { Width: cols, Height: rows };
|
|
if (!isEqual(this.size, newSize)) {
|
|
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
|
|
this.size = newSize;
|
|
}
|
|
}
|
|
|
|
protected parseMessage(data: string) {
|
|
data = data.substr(1); // skip channel
|
|
return base64.decode(data);
|
|
}
|
|
|
|
protected _onOpen(evt: Event) {
|
|
// Client should send terminal size in special channel 4,
|
|
// But this size will be changed by terminal.fit()
|
|
this.sendTerminalSize(120, 80);
|
|
super._onOpen(evt);
|
|
}
|
|
|
|
protected _onClose(evt: CloseEvent) {
|
|
const { code, reason, wasClean } = evt;
|
|
if (code !== 1000 || !wasClean) {
|
|
this.emitStatus("\r\n");
|
|
this.emitError(`Closed by "${reason}" (code: ${code}) at ${new Date()}.`);
|
|
}
|
|
super._onClose(evt);
|
|
this.isReady = false;
|
|
}
|
|
|
|
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) {
|
|
const { color, showTime } = options;
|
|
if (color) {
|
|
data = `${color}${data}${TerminalColor.NO_COLOR}`;
|
|
}
|
|
let time;
|
|
if (showTime) {
|
|
time = (new Date()).toLocaleString() + " ";
|
|
}
|
|
this.onData.emit(`${showTime ? time : ""}${data}\r\n`);
|
|
}
|
|
|
|
protected emitError(error: string) {
|
|
this.emitStatus(error, {
|
|
color: TerminalColor.RED
|
|
});
|
|
}
|
|
}
|