mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 08:51:24 +00:00
Shell: Integrate Cloudshell to existing shells (#2098)
* first draft * refactored code * ux fix * add custom header support and fix ui * minor changes * hide last command also * remove logger * bug fixes * updated loick file * fix tests * moved files * update readme * documentation update * fix compilationerror * undefined check handle * format fix * format fix * fix lints * format fix * fix unrelatred test * code refator * fix format * ut fix * cgmanifest * Revert "cgmanifest" This reverts commit 2e76a6926ee0d3d4e0510f2e04e03446c2ca8c47. * fix snap * test fix * formatting code * updated xterm * include username in command * cloudshell add exit * fix test * format fix * tets fix * fix multiple open cloudshell calls * socket time out after 20 min * remove unused code * 120 min * Addressed comments
This commit is contained in:
207
src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx
Normal file
207
src/Explorer/Tabs/CloudShellTab/Utils/AttachAddOn.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { AbstractShellHandler } from "Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler";
|
||||
import { IDisposable, ITerminalAddon, Terminal } from "@xterm/xterm";
|
||||
|
||||
interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
startMarker?: string;
|
||||
shellHandler?: AbstractShellHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal addon that attaches a terminal to a WebSocket for bidirectional
|
||||
* communication with Azure CloudShell.
|
||||
*
|
||||
* Features:
|
||||
* - Manages bidirectional data flow between terminal and CloudShell WebSocket
|
||||
* - Processes special status messages within the data stream
|
||||
* - Controls terminal output display during shell initialization
|
||||
* - Supports shell-specific customizations via AbstractShellHandler
|
||||
*
|
||||
* @implements {ITerminalAddon}
|
||||
*/
|
||||
export class AttachAddon implements ITerminalAddon {
|
||||
private _socket: WebSocket;
|
||||
private _bidirectional: boolean;
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _socketData: string;
|
||||
|
||||
private _allowTerminalWrite: boolean = true;
|
||||
|
||||
private _startMarker: string;
|
||||
private _shellHandler: AbstractShellHandler;
|
||||
|
||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
||||
this._socket = socket;
|
||||
// always set binary type to arraybuffer, we do not handle blobs
|
||||
this._socket.binaryType = "arraybuffer";
|
||||
this._bidirectional = !(options && options.bidirectional === false);
|
||||
this._startMarker = options?.startMarker;
|
||||
this._shellHandler = options?.shellHandler;
|
||||
this._socketData = "";
|
||||
this._allowTerminalWrite = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the addon with the provided terminal
|
||||
*
|
||||
* Sets up event listeners for terminal input and WebSocket messages.
|
||||
* Links the terminal input to the WebSocket and vice versa.
|
||||
*
|
||||
* @param {Terminal} terminal - The XTerm terminal instance
|
||||
*/
|
||||
public activate(terminal: Terminal): void {
|
||||
this.addMessageListener(terminal);
|
||||
if (this._bidirectional) {
|
||||
this._disposables.push(terminal.onData((data) => this._sendData(data)));
|
||||
this._disposables.push(terminal.onBinary((data) => this._sendBinary(data)));
|
||||
}
|
||||
|
||||
this._disposables.push(addSocketListener(this._socket, "close", () => this.dispose()));
|
||||
this._disposables.push(addSocketListener(this._socket, "error", () => this.dispose()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message listener to process data from the WebSocket
|
||||
*
|
||||
* Handles:
|
||||
* - Status message extraction (between ie_us and ie_ue markers)
|
||||
* - Partial message accumulation
|
||||
* - Shell initialization messages
|
||||
* - Suppression of unwanted shell output
|
||||
*
|
||||
* @param {Terminal} terminal - The XTerm terminal instance
|
||||
*/
|
||||
public addMessageListener(terminal: Terminal): void {
|
||||
this._disposables.push(
|
||||
addSocketListener(this._socket, "message", (ev) => {
|
||||
let data: ArrayBuffer | string = ev.data;
|
||||
const startStatusJson = "ie_us";
|
||||
const endStatusJson = "ie_ue";
|
||||
|
||||
if (typeof data === "object") {
|
||||
const enc = new TextDecoder("utf-8");
|
||||
data = enc.decode(ev.data as ArrayBuffer);
|
||||
}
|
||||
|
||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
||||
// process as one line
|
||||
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
|
||||
data = data.replace(statusData, "");
|
||||
data = data.replace(startStatusJson, "");
|
||||
data = data.replace(endStatusJson, "");
|
||||
} else if (data.includes(startStatusJson)) {
|
||||
// check for start
|
||||
const partialStatusData = data.split(startStatusJson)[1];
|
||||
this._socketData += partialStatusData;
|
||||
data = data.replace(partialStatusData, "");
|
||||
data = data.replace(startStatusJson, "");
|
||||
} else if (data.includes(endStatusJson)) {
|
||||
// check for end and process the command
|
||||
const partialStatusData = data.split(endStatusJson)[0];
|
||||
this._socketData += partialStatusData;
|
||||
data = data.replace(partialStatusData, "");
|
||||
data = data.replace(endStatusJson, "");
|
||||
this._socketData = "";
|
||||
} else if (this._socketData.length > 0) {
|
||||
// check if the line is all data then just concatenate
|
||||
this._socketData += data;
|
||||
data = "";
|
||||
}
|
||||
|
||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
||||
this._allowTerminalWrite = false;
|
||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
||||
}
|
||||
|
||||
if (this._allowTerminalWrite) {
|
||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
||||
|
||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
||||
terminal.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.includes(this._shellHandler.getConnectionCommand())) {
|
||||
this._allowTerminalWrite = true;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends string data from the terminal to the WebSocket
|
||||
*
|
||||
* @param {string} data - The data to send
|
||||
*/
|
||||
private _sendData(data: string): void {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends binary data from the terminal to the WebSocket
|
||||
*
|
||||
* @param {string} data - The string data to convert to binary and send
|
||||
*/
|
||||
private _sendBinary(data: string): void {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
const buffer = new Uint8Array(data.length);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
buffer[i] = data.charCodeAt(i) & 255;
|
||||
}
|
||||
this._socket.send(buffer);
|
||||
}
|
||||
|
||||
private _checkOpenSocket(): boolean {
|
||||
switch (this._socket.readyState) {
|
||||
case WebSocket.OPEN:
|
||||
return true;
|
||||
case WebSocket.CONNECTING:
|
||||
throw new Error("Attach addon was loaded before socket was open");
|
||||
case WebSocket.CLOSING:
|
||||
return false;
|
||||
case WebSocket.CLOSED:
|
||||
throw new Error("Attach addon socket is closed");
|
||||
default:
|
||||
throw new Error("Unexpected socket state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to a WebSocket and returns a disposable object
|
||||
* for cleanup
|
||||
*
|
||||
* @param {WebSocket} socket - The WebSocket instance
|
||||
* @param {K} type - The event type to listen for
|
||||
* @param {Function} handler - The event handler function
|
||||
* @returns {IDisposable} An object with a dispose method to remove the listener
|
||||
*/
|
||||
function addSocketListener<K extends keyof WebSocketEventMap>(
|
||||
socket: WebSocket,
|
||||
type: K,
|
||||
handler: (this: WebSocket, ev: WebSocketEventMap[K]) => void,
|
||||
): IDisposable {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user