export const protocols = ["webtty"]; export const msgInputUnknown = '0'; export const msgInput = '1'; export const msgPing = '2'; export const msgResizeTerminal = '3'; export const msgUnknownOutput = '0'; export const msgOutput = '1'; export const msgPong = '2'; export const msgSetWindowTitle = '3'; export const msgSetPreferences = '4'; export const msgSetReconnect = '5'; export const msgSetBufferSize = '6'; export interface Terminal { info(): { columns: number, rows: number }; output(data: string): void; showMessage(message: string, timeout: number): void; removeMessage(): void; setWindowTitle(title: string): void; setPreferences(value: object): void; onInput(callback: (input: string) => void): void; onResize(callback: (colmuns: number, rows: number) => void): void; reset(): void; deactivate(): void; close(): void; } export interface Connection { open(): void; close(): void; send(data: string): void; isOpen(): boolean; onOpen(callback: () => void): void; onReceive(callback: (data: string) => void): void; onClose(callback: () => void): void; } export interface ConnectionFactory { create(): Connection; } export class WebTTY { term: Terminal; connectionFactory: ConnectionFactory; args: string; authToken: string; reconnect: number; bufSize: number; constructor(term: Terminal, connectionFactory: ConnectionFactory, args: string, authToken: string) { this.term = term; this.connectionFactory = connectionFactory; this.args = args; this.authToken = authToken; this.reconnect = -1; this.bufSize = 1024; }; open() { let connection = this.connectionFactory.create(); let pingTimer: NodeJS.Timeout; let reconnectTimeout: NodeJS.Timeout; const setup = () => { connection.onOpen(() => { const termInfo = this.term.info(); connection.send(JSON.stringify( { Arguments: this.args, AuthToken: this.authToken, } )); const resizeHandler = (colmuns: number, rows: number) => { connection.send( msgResizeTerminal + JSON.stringify( { columns: colmuns, rows: rows } ) ); }; this.term.onResize(resizeHandler); resizeHandler(termInfo.columns, termInfo.rows); this.term.onInput( (input: string) => { // Leave room for message type id let effectiveBufferSize = this.bufSize - 1; // Split input into buffer sized chunks for (let i = 0; i < Math.ceil(input.length/effectiveBufferSize); i++) { let inputChunk = input.substring(i*effectiveBufferSize, Math.min((i+1)*effectiveBufferSize, input.length)) connection.send(msgInput + inputChunk); } } ); pingTimer = setInterval(() => { connection.send(msgPing) }, 30 * 1000); }); connection.onReceive((data) => { const payload = data.slice(1); switch (data[0]) { case msgOutput: this.term.output(atob(payload)); break; case msgPong: break; case msgSetWindowTitle: this.term.setWindowTitle(payload); break; case msgSetPreferences: const preferences = JSON.parse(payload); this.term.setPreferences(preferences); break; case msgSetReconnect: const autoReconnect = JSON.parse(payload); console.log("Enabling reconnect: " + autoReconnect + " seconds") this.reconnect = autoReconnect; break; case msgSetBufferSize: const bufSize = JSON.parse(payload); this.bufSize = bufSize; break; } }); connection.onClose(() => { clearInterval(pingTimer); this.term.deactivate(); this.term.showMessage("Connection Closed", 0); if (this.reconnect > 0) { reconnectTimeout = setTimeout(() => { connection = this.connectionFactory.create(); this.term.reset(); setup(); }, this.reconnect * 1000); } }); connection.open(); } setup(); return () => { clearTimeout(reconnectTimeout); connection.close(); } }; };