From 45cd98ef001fbd7127d189e215ea3e7649174e4b Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Jun 2022 00:31:54 -0700 Subject: [PATCH] refactor term/ws into separate modules --- src/main.tsx | 199 +++++---------------------------------------------- src/sh2.ts | 2 + src/term.ts | 91 +++++++++++++++++++++++ src/ws.ts | 143 ++++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 181 deletions(-) create mode 100644 src/term.ts create mode 100644 src/ws.ts diff --git a/src/main.tsx b/src/main.tsx index 715966543..3fc2e1a2d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,12 +1,13 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; import * as mobx from "mobx"; -import {Terminal} from 'xterm'; import {sprintf} from "sprintf-js"; import {boundMethod} from "autobind-decorator"; import * as dayjs from 'dayjs' import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components"; import cn from "classnames" +import {GlobalWS} from "./ws"; +import {TermWrap} from "./term"; type LineType = { lineid : number, @@ -24,8 +25,6 @@ var GlobalLines = mobx.observable.box([ {lineid: 2, userid: "sawka", ts: 1654631125000, linetype: "text", text: "again"}, ]); -var GlobalWS : any = null; - var TermMap = {}; window.TermMap = TermMap; @@ -99,43 +98,21 @@ class LineText extends React.Component<{line : LineType}, {}> { } } -function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, callback?: () => void) { - term.clear() - let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", sessionId, cmdId); - fetch(url).then((resp) => { - if (!resp.ok) { - throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText)); - } - return resp.text() - }).then((resptext) => { - setTimeout(() => term.write(resptext, callback), 0); - }); -} - @mobxReact.observer class LineCmd extends React.Component<{line : LineType}, {}> { - terminal : mobx.IObservableValue = mobx.observable.box(null, {name: "terminal"}); - focus : mobx.IObservableValue = mobx.observable.box(false, {name: "focus"}); - version : mobx.IObservableValue = mobx.observable.box(0, {name: "lineversion"}); + termWrap : TermWrap; + + constructor(props) { + super(props); + let {line, sessionid} = this.props; + this.termWrap = new TermWrap(sessionid, line.cmdid); + } componentDidMount() { let {line, sessionid} = this.props; - let terminal = new Terminal({rows: 2, cols: 80}); - TermMap[line.cmdid] = terminal; let termElem = document.getElementById(this.getId()); - terminal.open(termElem); - mobx.action(() => this.terminal.set(terminal))(); - this.reloadTerminal(); - terminal.textarea.addEventListener("focus", () => { - mobx.action(() => { - this.focus.set(true); - })(); - }); - terminal.textarea.addEventListener("blur", () => { - mobx.action(() => { - this.focus.set(false); - })(); - }); + this.termWrap.connectToElem(termElem); + this.termWrap.reloadTerminal(); if (line.isnew) { setTimeout(() => { let lineElem = document.getElementById("line-" + this.getId()); @@ -145,22 +122,11 @@ class LineCmd extends React.Component<{line : LineType}, {}> { })(); }, 100); setTimeout(() => { - this.reloadTerminal(); + this.termWrap.reloadTerminal(); }, 1000); } } - reloadTerminal() { - let {line, sessionid} = this.props; - let terminal = this.terminal.get(); - loadPtyOut(terminal, sessionid, line.cmdid, this.incVersion); - } - - @boundMethod - incVersion() : void { - mobx.action(() => this.version.set(this.version.get() + 1))(); - } - getId() : string { let {line} = this.props; return "cmd-" + line.lineid + "-" + line.cmdid; @@ -168,7 +134,7 @@ class LineCmd extends React.Component<{line : LineType}, {}> { @boundMethod doRefresh() { - this.reloadTerminal(); + this.termWRap.reloadTerminal(); } @boundMethod @@ -191,21 +157,11 @@ class LineCmd extends React.Component<{line : LineType}, {}> { let {line} = this.props; let lineid = line.lineid.toString(); let running = false; - let term = this.terminal.get(); - let version = this.version.get(); let rows = 0; let cols = 0; - if (term != null) { - let termNumLines = term._core.buffer.lines.length; - let termYPos = term._core.buffer.y; - if (term.rows < 25 && termNumLines > term.rows) { - term.resize(80, Math.min(25, termNumLines)); - } else if (term.rows < 25 && termYPos >= term.rows) { - term.resize(80, Math.min(25, termYPos+1)); - } - rows = term.rows; - cols = term.cols; - } + let renderVersion = this.termWrap.getRenderVersion(); + this.termWrap.resizeToContent(); + let termSize = this.termWrap.getSize(); return (
= 5}, {"running": running})}> @@ -215,10 +171,10 @@ class LineCmd extends React.Component<{line : LineType}, {}> {
{line.userid}
{dayjs(line.ts).format("hh:mm:ss a")}
-
{line.cmdid} 0}>({rows}x{cols}) v{version}
+
{line.cmdid} 0}>({termSize.rows}x{termSize.cols}) v{renderVersion}
> {this.singleLineCmdText(line.cmdtext)}
-
+
@@ -277,7 +233,6 @@ class CmdInput extends React.Component<{line : LineType, sessionid : string}, {} let url = sprintf("http://localhost:8080/api/run-command"); let data = {sessionid: this.props.sessionid, command: commandStr}; fetch(url, {method: "post", body: JSON.stringify(data)}).then((resp) => handleJsonFetchResponse(url, resp)).then((data) => { - console.log("got success data", data); mobx.action(() => { let lines = GlobalLines.get(); data.data.line.isnew = true; @@ -333,131 +288,13 @@ class SessionView extends React.Component<{sessionid : string}, {}> { } } -class WSControl { - wsConn : any; - openCallback : any; - open : boolean; - opening : boolean; - reconnectTimes : int; - - constructor(openCallback : any) { - this.reconnectTimes = 0; - this.open = false; - this.opening = false; - this.openCallback = openCallback; - setInterval(this.sendPing, 5000); - this.reconnect(); - } - - reconnect() { - if (this.open) { - this.wsConn.close(); - return; - } - this.reconnectTimes++; - let timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60]; - let timeout = 60; - if (this.reconnectTimes < timeoutArr.length) { - timeout = timeoutArr[this.reconnectTimes]; - } - if (timeout > 0 || true) { - console.log(sprintf("websocket reconnect(%d), sleep %ds", this.reconnectTimes, timeout)); - } - setTimeout(() => { - console.log(sprintf("websocket reconnect(%d)", this.reconnectTimes)); - this.opening = true; - this.wsConn = new WebSocket("ws://localhost:8081/ws"); - this.wsConn.onopen = this.onopen; - this.wsConn.onmessage = this.onmessage; - this.wsConn.onerror = this.onerror; - this.wsConn.onclose = this.onclose; - }, timeout*1000); - } - - @boundMethod - onerror(event : any) { - console.log("websocket error", event); - if (this.open || this.opening) { - this.open = false; - this.opening = false; - this.reconnect(); - } - } - - @boundMethod - onclose(event : any) { - console.log("websocket closed", event); - if (this.open || this.opening) { - this.open = false; - this.opening = false; - this.reconnect(); - } - } - - @boundMethod - onopen() { - console.log("websocket open"); - this.open = true; - this.opening = false; - this.reconnectTimes = 0; - if (this.openCallback != null) { - this.openCallback(); - } - } - - @boundMethod - onmessage(event : any) { - let eventData = null; - if (event.data != null) { - eventData = JSON.parse(event.data); - } - if (eventData == null) { - return; - } - if (eventData.type == "ping") { - this.wsConn.send(JSON.stringify({type: "pong", stime: parseInt(Date.now()/1000)})); - return; - } - if (eventData.type == "pong") { - // nothing - return; - } - console.log("websocket message", event); - } - - @boundMethod - sendPing() { - if (!this.open) { - return; - } - this.wsConn.send(JSON.stringify({type: "ping", stime: Date.now()})); - } - - sendMessage(data : any){ - if (!this.open) { - return; - } - this.wsConn.send(JSON.stringify(data)); - } -} - @mobxReact.observer class Main extends React.Component<{sessionid : string}, {}> { - version : mobx.IObservableValue = mobx.observable.box(false); - constructor(props : any) { super(props); - GlobalWS = new WSControl(this.updateVersion); - window.GlobalWS = GlobalWS; } - @boundMethod - updateVersion() { - mobx.action(() => this.version.set(this.version.get()+1))(); - } - render() { - let version = this.version.get(); return (

diff --git a/src/sh2.ts b/src/sh2.ts index 1f9756ee8..c602e8982 100644 --- a/src/sh2.ts +++ b/src/sh2.ts @@ -3,12 +3,14 @@ import {createRoot} from 'react-dom/client'; import {sprintf} from "sprintf-js"; import {Terminal} from 'xterm'; import {Main} from "./main"; +import {GlobalWS} from "./ws"; let VERSION = __SHVERSION__; let terminal = null; let sessionId = "47445c53-cfcf-4943-8339-2c04447f20a1"; document.addEventListener("DOMContentLoaded", () => { + GlobalWS.reconnect(); let reactElem = React.createElement(Main, {sessionid: sessionId}, null); let elem = document.getElementById("main"); let root = createRoot(elem); diff --git a/src/term.ts b/src/term.ts new file mode 100644 index 000000000..c927aa0d9 --- /dev/null +++ b/src/term.ts @@ -0,0 +1,91 @@ +import * as mobx from "mobx"; +import {Terminal} from 'xterm'; +import {sprintf} from "sprintf-js"; +import {boundMethod} from "autobind-decorator"; +import {GlobalWS} from "./ws"; + +var TermMap : Record; + +function loadPtyOut(term : Terminal, sessionId : string, cmdId : string, callback?: () => void) { + term.clear() + let url = sprintf("http://localhost:8080/api/ptyout?sessionid=%s&cmdid=%s", sessionId, cmdId); + fetch(url).then((resp) => { + if (!resp.ok) { + throw new Error(sprintf("Bad fetch response for /api/ptyout: %d %s", resp.status, resp.statusText)); + } + return resp.text() + }).then((resptext) => { + setTimeout(() => term.write(resptext, callback), 0); + }); +} + +class TermWrap { + terminal : Terminal; + sessionId : string; + cmdId : string; + ptyPos : number; + runPos : number; + runData : string; + renderVersion : mobx.IObservableValue = mobx.observable.box(1, {name: "renderVersion"}); + isFocused : mobx.IObservableValue = mobx.observable.box(false, {name: "focus"}); + + constructor(sessionId : string, cmdId : string) { + this.terminal = new Terminal({rows: 2, cols: 80}); + this.sessionId = sessionId; + this.cmdId = cmdId; + this.ptyPos = 0; + this.runPos = 0; + this.runData = ""; + TermMap[cmdId] = this; + } + + resizeToContent() { + let term = this.terminal; + let termNumLines = term._core.buffer.lines.length; + let termYPos = term._core.buffer.y; + if (term.rows < 25 && termNumLines > term.rows) { + term.resize(80, Math.min(25, termNumLines)); + } else if (term.rows < 25 && termYPos >= term.rows) { + term.resize(80, Math.min(25, termYPos+1)); + } + } + + getSize() : {rows : number, cols : number} { + return {rows: this.terminal.rows, cols: this.terminal.cols}; + } + + @boundMethod + setFocus(val : boolean) { + mobx.action(() => this.isFocused.set(val))(); + } + + getRenderVersion() : number { + return this.renderVersion.get(); + } + + @boundMethod + incRenderVersion() { + mobx.action(() => this.renderVersion.set(this.renderVersion.get() + 1))(); + } + + reloadTerminal() { + loadPtyOut(this.terminal, this.sessionId, this.cmdId, this.incRenderVersion); + } + + connectToElem(elem : Element) { + this.terminal.open(elem); + this.terminal.textarea.addEventListener("focus", () => { + this.setFocus(true); + }); + this.terminal.textarea.addEventListener("blur", () => { + this.setFocus(false); + }); + } +} + +if (window.TermMap == null) { + TermMap = {}; + window.TermMap = TermMap; +} + +export {TermWrap, TermMap}; diff --git a/src/ws.ts b/src/ws.ts new file mode 100644 index 000000000..5df699154 --- /dev/null +++ b/src/ws.ts @@ -0,0 +1,143 @@ +import * as mobx from "mobx"; +import {sprintf} from "sprintf-js"; +import {boundMethod} from "autobind-decorator"; + +class WSControl { + wsConn : any; + open : mobx.IObservableValue; + opening : boolean; + reconnectTimes : int; + msgQueue : any[]; + + constructor() { + this.reconnectTimes = 0; + this.open = mobx.observable.box(false, {name: "WSOpen"}); + this.opening = false; + this.msgQueue = []; + setInterval(this.sendPing, 5000); + } + + @mobx.action + setOpen(val : boolean) { + this.open.set(val); + } + + reconnect() { + if (this.open.get()) { + this.wsConn.close(); + return; + } + this.reconnectTimes++; + let timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60]; + let timeout = 60; + if (this.reconnectTimes < timeoutArr.length) { + timeout = timeoutArr[this.reconnectTimes]; + } + if (timeout > 0) { + console.log(sprintf("websocket reconnect(%d), sleep %ds", this.reconnectTimes, timeout)); + } + setTimeout(() => { + console.log(sprintf("websocket reconnect(%d)", this.reconnectTimes)); + this.opening = true; + this.wsConn = new WebSocket("ws://localhost:8081/ws"); + this.wsConn.onopen = this.onopen; + this.wsConn.onmessage = this.onmessage; + this.wsConn.onerror = this.onerror; + this.wsConn.onclose = this.onclose; + }, timeout*1000); + } + + @boundMethod + onerror(event : any) { + console.log("websocket error", event); + if (this.open.get() || this.opening) { + this.setOpen(false); + this.opening = false; + this.reconnect(); + } + } + + @boundMethod + onclose(event : any) { + console.log("websocket closed", event); + if (this.open.get() || this.opening) { + this.setOpen(false); + this.opening = false; + this.reconnect(); + } + } + + @boundMethod + onopen() { + console.log("websocket open"); + this.setOpen(true); + this.opening = false; + this.reconnectTimes = 0; + this.runMsgQueue(); + } + + runMsgQueue() { + if (!this.open.get()) { + return; + } + if (this.msgQueue.length == 0) { + return; + } + let msg = this.msgQueue.shift(); + this.sendMessage(msg); + setTimeout(() => { + this.runMsgQueue(); + }, 100); + } + + @boundMethod + onmessage(event : any) { + let eventData = null; + if (event.data != null) { + eventData = JSON.parse(event.data); + } + if (eventData == null) { + return; + } + if (eventData.type == "ping") { + this.wsConn.send(JSON.stringify({type: "pong", stime: parseInt(Date.now()/1000)})); + return; + } + if (eventData.type == "pong") { + // nothing + return; + } + console.log("websocket message", event); + } + + @boundMethod + sendPing() { + if (!this.open.get()) { + return; + } + this.wsConn.send(JSON.stringify({type: "ping", stime: Date.now()})); + } + + sendMessage(data : any) { + if (!this.open.get()) { + return; + } + this.wsConn.send(JSON.stringify(data)); + } + + pushMessage(data : any) { + if (!this.open.get()) { + this.msgQueue.push(data); + return; + } + this.sendMessage(data); + } +} + +var GlobalWS : WSControl; +if (window.GlobalWS == null) { + GlobalWS = new WSControl(); + window.GlobalWS = GlobalWS; +} + +export {GlobalWS};