183 lines
4.7 KiB
JavaScript
183 lines
4.7 KiB
JavaScript
|
|
/**
|
||
|
|
* Terminado WebSocket Addon for xterm.js 5.x
|
||
|
|
*
|
||
|
|
* This addon handles the Terminado protocol for xterm.js, allowing
|
||
|
|
* bidirectional communication over WebSocket with a backend PTY process.
|
||
|
|
*
|
||
|
|
* The Terminado protocol uses JSON arrays:
|
||
|
|
* - ["stdin", data] - send input to the terminal
|
||
|
|
* - ["stdout", data] - receive output from the terminal
|
||
|
|
* - ["set_size", rows, cols] - notify backend of terminal size changes
|
||
|
|
*/
|
||
|
|
|
||
|
|
class TerminadoAddon {
|
||
|
|
constructor() {
|
||
|
|
this._disposables = [];
|
||
|
|
this._socket = null;
|
||
|
|
this._bidirectional = true;
|
||
|
|
this._buffered = true;
|
||
|
|
this._attachSocketBuffer = '';
|
||
|
|
this._flushTimeout = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
activate(terminal) {
|
||
|
|
this._terminal = terminal;
|
||
|
|
}
|
||
|
|
|
||
|
|
dispose() {
|
||
|
|
this._disposables.forEach(d => d.dispose());
|
||
|
|
this._disposables.length = 0;
|
||
|
|
|
||
|
|
if (this._flushTimeout) {
|
||
|
|
clearTimeout(this._flushTimeout);
|
||
|
|
this._flushTimeout = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._socket) {
|
||
|
|
this.detach();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
attach(socket, bidirectional = true, buffered = true) {
|
||
|
|
if (this._socket) {
|
||
|
|
this.detach();
|
||
|
|
}
|
||
|
|
|
||
|
|
this._socket = socket;
|
||
|
|
this._bidirectional = bidirectional !== false;
|
||
|
|
this._buffered = buffered !== false;
|
||
|
|
|
||
|
|
// Handle incoming messages from the websocket
|
||
|
|
this._messageHandler = (ev) => {
|
||
|
|
try {
|
||
|
|
const data = JSON.parse(ev.data);
|
||
|
|
if (Array.isArray(data) && data[0] === 'stdout') {
|
||
|
|
const output = data[1];
|
||
|
|
if (this._buffered) {
|
||
|
|
this._pushToBuffer(output);
|
||
|
|
} else {
|
||
|
|
this._terminal.write(output);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error handling terminado message:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle terminal input (user typing)
|
||
|
|
if (this._bidirectional) {
|
||
|
|
const dataDisposable = this._terminal.onData((data) => {
|
||
|
|
this._sendData(data);
|
||
|
|
});
|
||
|
|
this._disposables.push(dataDisposable);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle terminal resize events
|
||
|
|
const resizeDisposable = this._terminal.onResize((size) => {
|
||
|
|
this._setSize(size);
|
||
|
|
});
|
||
|
|
this._disposables.push(resizeDisposable);
|
||
|
|
|
||
|
|
// Handle socket close/error
|
||
|
|
this._closeHandler = () => this.detach();
|
||
|
|
this._errorHandler = () => this.detach();
|
||
|
|
|
||
|
|
socket.addEventListener('message', this._messageHandler);
|
||
|
|
socket.addEventListener('close', this._closeHandler);
|
||
|
|
socket.addEventListener('error', this._errorHandler);
|
||
|
|
}
|
||
|
|
|
||
|
|
detach() {
|
||
|
|
if (!this._socket) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._messageHandler) {
|
||
|
|
this._socket.removeEventListener('message', this._messageHandler);
|
||
|
|
this._messageHandler = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._closeHandler) {
|
||
|
|
this._socket.removeEventListener('close', this._closeHandler);
|
||
|
|
this._closeHandler = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this._errorHandler) {
|
||
|
|
this._socket.removeEventListener('error', this._errorHandler);
|
||
|
|
this._errorHandler = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._disposables.forEach(d => d.dispose());
|
||
|
|
this._disposables.length = 0;
|
||
|
|
|
||
|
|
this._socket = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
_sendData(data) {
|
||
|
|
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||
|
|
try {
|
||
|
|
this._socket.send(JSON.stringify(['stdin', data]));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error sending data to terminal:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_setSize(size) {
|
||
|
|
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||
|
|
try {
|
||
|
|
this._socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error sending terminal size:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_pushToBuffer(data) {
|
||
|
|
if (this._attachSocketBuffer) {
|
||
|
|
this._attachSocketBuffer += data;
|
||
|
|
} else {
|
||
|
|
this._attachSocketBuffer = data;
|
||
|
|
if (this._flushTimeout) {
|
||
|
|
clearTimeout(this._flushTimeout);
|
||
|
|
}
|
||
|
|
this._flushTimeout = setTimeout(() => this._flushBuffer(), 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_flushBuffer() {
|
||
|
|
if (this._attachSocketBuffer && this._terminal) {
|
||
|
|
this._terminal.write(this._attachSocketBuffer);
|
||
|
|
this._attachSocketBuffer = '';
|
||
|
|
}
|
||
|
|
this._flushTimeout = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Public method to manually send size
|
||
|
|
sendSize(rows, cols) {
|
||
|
|
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||
|
|
try {
|
||
|
|
this._socket.send(JSON.stringify(['set_size', rows, cols]));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error sending manual terminal size:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Public method to manually send command
|
||
|
|
sendCommand(command) {
|
||
|
|
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
|
||
|
|
try {
|
||
|
|
this._socket.send(JSON.stringify(['stdin', command]));
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error sending command:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Export for use in browsers
|
||
|
|
if (typeof module !== 'undefined' && module.exports) {
|
||
|
|
module.exports = TerminadoAddon;
|
||
|
|
}
|