mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
Screen MemStore (#197)
* working on an in-memory store for screen information * nostrpos sentinel * textareainput now tracks selection (to update backend) * make websocket connections much safer. add a defer/panic handler for each ws message handled on backend. don't allow client to reconnect to backend ws handler more than once per second (handles issue with lots of fast fails) * use onSelect to have frontend textarea sync state to backend ScreenMem store * restore cmdline when switching screens * prettier
This commit is contained in:
parent
9980a6b204
commit
6a1b2c8bd4
@ -11,3 +11,5 @@ export const CLIENT_SETTINGS = "clientSettings";
|
||||
export const LineContainer_Main = "main";
|
||||
export const LineContainer_History = "history";
|
||||
export const LineContainer_Sidebar = "sidebar";
|
||||
|
||||
export const NoStrPos = -1;
|
||||
|
@ -127,10 +127,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={this.cmdInputRef}
|
||||
className={cn("cmd-input", { "has-info": infoShow }, { active: focusVal })}
|
||||
>
|
||||
<div ref={this.cmdInputRef} className={cn("cmd-input", { "has-info": infoShow }, { active: focusVal })}>
|
||||
<If condition={historyShow}>
|
||||
<div className="cmd-input-grow-spacer"></div>
|
||||
<HistoryInfo />
|
||||
@ -153,10 +150,12 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
</If>
|
||||
<div key="prompt" className="cmd-input-context">
|
||||
<div className="has-text-white">
|
||||
<span ref={this.promptRef}><Prompt rptr={rptr} festate={feState} /></span>
|
||||
<span ref={this.promptRef}>
|
||||
<Prompt rptr={rptr} festate={feState} />
|
||||
</span>
|
||||
</div>
|
||||
<If condition={numRunningLines > 0}>
|
||||
<div onClick={() => this.toggleFilter(screen)}className="cmd-input-filter">
|
||||
<div onClick={() => this.toggleFilter(screen)} className="cmd-input-filter">
|
||||
{numRunningLines}
|
||||
<div className="avatar">
|
||||
<RotateIcon className="warning spin" />
|
||||
@ -176,7 +175,11 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
<div className="button is-static">{inputMode}</div>
|
||||
</div>
|
||||
</If>
|
||||
<TextAreaInput key={textAreaInputKey} onHeightChange={this.handleInnerHeightUpdate} />
|
||||
<TextAreaInput
|
||||
key={textAreaInputKey}
|
||||
screen={screen}
|
||||
onHeightChange={this.handleInnerHeightUpdate}
|
||||
/>
|
||||
<div className="control cmd-exec">
|
||||
{/**<div onClick={inputModel.toggleExpandInput} className="hint-item color-white">
|
||||
{inputModel.inputExpanded.get() ? "shrink" : "expand"} input ({renderCmdText("E")})
|
||||
|
@ -4,11 +4,15 @@
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import type * as T from "../../../types/types";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
import { getMonoFontSize } from "../../../util/textmeasure";
|
||||
import { isModKeyPress, hasNoModifiers } from "../../../util/util";
|
||||
import * as appconst from "../../appconst";
|
||||
|
||||
type OV<T> = mobx.IObservableValue<T>;
|
||||
|
||||
function pageSize(div: any): number {
|
||||
if (div == null) {
|
||||
@ -35,21 +39,44 @@ function scrollDiv(div: any, amt: number) {
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}> {
|
||||
class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () => void }, {}> {
|
||||
lastTab: boolean = false;
|
||||
lastHistoryUpDown: boolean = false;
|
||||
lastTabCurLine: mobx.IObservableValue<string> = mobx.observable.box(null);
|
||||
lastTabCurLine: OV<string> = mobx.observable.box(null);
|
||||
lastFocusType: string = null;
|
||||
mainInputRef: React.RefObject<any>;
|
||||
historyInputRef: React.RefObject<any>;
|
||||
controlRef: React.RefObject<any>;
|
||||
mainInputRef: React.RefObject<HTMLTextAreaElement> = React.createRef();
|
||||
historyInputRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
controlRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
lastHeight: number = 0;
|
||||
lastSP: T.StrWithPos = { str: "", pos: appconst.NoStrPos };
|
||||
version: OV<number> = mobx.observable.box(0); // forces render updates
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.mainInputRef = React.createRef();
|
||||
this.historyInputRef = React.createRef();
|
||||
this.controlRef = React.createRef();
|
||||
incVersion(): void {
|
||||
let v = this.version.get();
|
||||
mobx.action(() => this.version.set(v + 1))();
|
||||
}
|
||||
|
||||
getCurSP(): T.StrWithPos {
|
||||
let textarea = this.mainInputRef.current;
|
||||
if (textarea == null) {
|
||||
return this.lastSP;
|
||||
}
|
||||
let str = textarea.value;
|
||||
let pos = textarea.selectionStart;
|
||||
let endPos = textarea.selectionEnd;
|
||||
if (pos != endPos) {
|
||||
return { str, pos: appconst.NoStrPos };
|
||||
}
|
||||
return { str, pos };
|
||||
}
|
||||
|
||||
updateSP(): void {
|
||||
let curSP = this.getCurSP();
|
||||
if (curSP.str == this.lastSP.str && curSP.pos == this.lastSP.pos) {
|
||||
return;
|
||||
}
|
||||
this.lastSP = curSP;
|
||||
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
|
||||
}
|
||||
|
||||
setFocus(): void {
|
||||
@ -100,6 +127,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
this.lastFocusType = focusType;
|
||||
}
|
||||
this.checkHeight(false);
|
||||
this.updateSP();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@ -112,10 +140,11 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
this.lastFocusType = focusType;
|
||||
}
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
if (inputModel.forceCursorPos.get() != null) {
|
||||
let fcpos = inputModel.forceCursorPos.get();
|
||||
if (fcpos != null && fcpos != appconst.NoStrPos) {
|
||||
if (this.mainInputRef.current != null) {
|
||||
this.mainInputRef.current.selectionStart = inputModel.forceCursorPos.get();
|
||||
this.mainInputRef.current.selectionEnd = inputModel.forceCursorPos.get();
|
||||
this.mainInputRef.current.selectionStart = fcpos;
|
||||
this.mainInputRef.current.selectionEnd = fcpos;
|
||||
}
|
||||
mobx.action(() => inputModel.forceCursorPos.set(null))();
|
||||
}
|
||||
@ -124,6 +153,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
this.setFocus();
|
||||
}
|
||||
this.checkHeight(true);
|
||||
this.updateSP();
|
||||
}
|
||||
|
||||
getLinePos(elem: any): { numLines: number; linePos: number } {
|
||||
@ -294,6 +324,11 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onSelect(e: any) {
|
||||
this.incVersion();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onHistoryKeyDown(e: any) {
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
@ -386,7 +421,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
}
|
||||
let cutValue = value.substr(0, selStart);
|
||||
let restValue = value.substr(selStart);
|
||||
let cmdLineUpdate = { cmdline: restValue, cursorpos: 0 };
|
||||
let cmdLineUpdate = { str: restValue, pos: 0 };
|
||||
navigator.clipboard.writeText(cutValue);
|
||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||
}
|
||||
@ -439,7 +474,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
let cutValue = value.slice(cutSpot, selStart);
|
||||
let prevValue = value.slice(0, cutSpot);
|
||||
let restValue = value.slice(selStart);
|
||||
let cmdLineUpdate = { cmdline: prevValue + restValue, cursorpos: prevValue.length };
|
||||
let cmdLineUpdate = { str: prevValue + restValue, pos: prevValue.length };
|
||||
navigator.clipboard.writeText(cutValue);
|
||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||
}
|
||||
@ -459,7 +494,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
return;
|
||||
}
|
||||
let newValue = value.substr(0, selStart) + clipText + value.substr(selEnd);
|
||||
let cmdLineUpdate = { cmdline: newValue, cursorpos: selStart + clipText.length };
|
||||
let cmdLineUpdate = { str: newValue, pos: selStart + clipText.length };
|
||||
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
|
||||
});
|
||||
}
|
||||
@ -525,6 +560,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
let numLines = curLine.split("\n").length;
|
||||
let maxCols = this.getTextAreaMaxCols();
|
||||
let longLine = false;
|
||||
let version = this.version.get(); // to force reactions
|
||||
if (maxCols != 0 && curLine.length >= maxCols - 4) {
|
||||
longLine = true;
|
||||
}
|
||||
@ -559,6 +595,7 @@ class TextAreaInput extends React.Component<{ onHeightChange: () => void }, {}>
|
||||
value={curLine}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onSelect={this.onSelect}
|
||||
className={cn("textarea", { "display-disabled": disabled })}
|
||||
></textarea>
|
||||
<input
|
||||
|
@ -1444,10 +1444,12 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
updateCmdLine(cmdLine: CmdLineUpdateType): void {
|
||||
updateCmdLine(cmdLine: T.StrWithPos): void {
|
||||
mobx.action(() => {
|
||||
this.setCurLine(cmdLine.cmdline);
|
||||
this.forceCursorPos.set(cmdLine.cursorpos);
|
||||
this.setCurLine(cmdLine.str);
|
||||
if (cmdLine.pos != appconst.NoStrPos) {
|
||||
this.forceCursorPos.set(cmdLine.pos);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@ -3164,6 +3166,7 @@ class Model {
|
||||
showLinks: OV<boolean> = mobx.observable.box(true, {
|
||||
name: "model-showLinks",
|
||||
});
|
||||
packetSeqNum: number = 0;
|
||||
|
||||
constructor() {
|
||||
this.clientId = getApi().getId();
|
||||
@ -3217,6 +3220,11 @@ class Model {
|
||||
setTimeout(() => this.getClientDataLoop(1), 10);
|
||||
}
|
||||
|
||||
getNextPacketSeqNum(): number {
|
||||
this.packetSeqNum++;
|
||||
return this.packetSeqNum;
|
||||
}
|
||||
|
||||
getPlatform(): string {
|
||||
if (this.platform != null) {
|
||||
return this.platform;
|
||||
@ -3618,6 +3626,12 @@ class Model {
|
||||
let newContext = this.getUIContext();
|
||||
if (oldContext.sessionid != newContext.sessionid || oldContext.screenid != newContext.screenid) {
|
||||
this.inputModel.resetInput();
|
||||
if ("cmdline" in genUpdate) {
|
||||
// TODO a bit of a hack since this update gets applied in runUpdate_internal.
|
||||
// we then undo that update with the resetInput, and then redo it with the line below
|
||||
// not sure how else to handle this for now though
|
||||
this.inputModel.updateCmdLine(genUpdate.cmdline);
|
||||
}
|
||||
} else if (remotePtrToString(oldContext.remote) != remotePtrToString(newContext.remote)) {
|
||||
this.inputModel.resetHistory();
|
||||
}
|
||||
@ -3996,10 +4010,7 @@ class Model {
|
||||
getActiveIds(): [string, string] {
|
||||
let activeSession = this.getActiveSession();
|
||||
let activeScreen = this.getActiveScreen();
|
||||
return [
|
||||
activeSession == null ? null : activeSession.sessionId,
|
||||
activeScreen == null ? null : activeScreen.screenId,
|
||||
];
|
||||
return [activeSession?.sessionId, activeScreen?.screenId];
|
||||
}
|
||||
|
||||
_loadScreenLinesAsync(newWin: ScreenLines) {
|
||||
@ -4118,6 +4129,16 @@ class Model {
|
||||
this.ws.pushMessage(inputPacket);
|
||||
}
|
||||
|
||||
sendCmdInputText(screenId: string, sp: T.StrWithPos) {
|
||||
let pk: T.CmdInputTextPacketType = {
|
||||
type: "cmdinputtext",
|
||||
seqnum: this.getNextPacketSeqNum(),
|
||||
screenid: screenId,
|
||||
text: sp,
|
||||
};
|
||||
this.ws.pushMessage(pk);
|
||||
}
|
||||
|
||||
resolveUserIdToName(userid: string): string {
|
||||
return "@[unknown]";
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class WSControl {
|
||||
wsLog: mobx.IObservableArray<string> = mobx.observable.array([], { name: "wsLog" });
|
||||
authKey: string;
|
||||
baseHostPort: string;
|
||||
lastReconnectTime: number = 0;
|
||||
|
||||
constructor(baseHostPort: string, clientId: string, authKey: string, messageCallback: (any) => void) {
|
||||
this.baseHostPort = baseHostPort;
|
||||
@ -51,6 +52,7 @@ class WSControl {
|
||||
if (this.open.get()) {
|
||||
return;
|
||||
}
|
||||
this.lastReconnectTime = Date.now();
|
||||
this.log(sprintf("try reconnect (%s)", desc));
|
||||
this.opening = true;
|
||||
this.wsConn = new WebSocket(this.baseHostPort + "/ws?clientid=" + this.clientId);
|
||||
@ -78,6 +80,9 @@ class WSControl {
|
||||
if (this.reconnectTimes < timeoutArr.length) {
|
||||
timeout = timeoutArr[this.reconnectTimes];
|
||||
}
|
||||
if (Date.now() - this.lastReconnectTime < 500) {
|
||||
timeout = 1;
|
||||
}
|
||||
if (timeout > 0) {
|
||||
this.log(sprintf("sleeping %ds", timeout));
|
||||
}
|
||||
|
@ -210,6 +210,13 @@ type WatchScreenPacketType = {
|
||||
authkey: string;
|
||||
};
|
||||
|
||||
type CmdInputTextPacketType = {
|
||||
type: string;
|
||||
seqnum: number;
|
||||
screenid: string;
|
||||
text: StrWithPos;
|
||||
};
|
||||
|
||||
type TermWinSize = {
|
||||
rows: number;
|
||||
cols: number;
|
||||
@ -267,7 +274,7 @@ type ModelUpdateType = {
|
||||
lines?: LineType[];
|
||||
cmd?: CmdDataType;
|
||||
info?: InfoType;
|
||||
cmdline?: CmdLineUpdateType;
|
||||
cmdline?: StrWithPos;
|
||||
remotes?: RemoteType[];
|
||||
history?: HistoryInfoType;
|
||||
connect?: boolean;
|
||||
@ -666,6 +673,11 @@ type ModalStoreEntry = {
|
||||
uniqueKey: string;
|
||||
};
|
||||
|
||||
type StrWithPos = {
|
||||
str: string;
|
||||
pos: number;
|
||||
};
|
||||
|
||||
export type {
|
||||
SessionDataType,
|
||||
LineStateType,
|
||||
@ -741,4 +753,6 @@ export type {
|
||||
ExtFile,
|
||||
LineContainerStrs,
|
||||
ModalStoreEntry,
|
||||
StrWithPos,
|
||||
CmdInputTextPacketType,
|
||||
};
|
||||
|
@ -2142,7 +2142,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
|
||||
return nil, nil
|
||||
}
|
||||
update := &sstore.ModelUpdate{
|
||||
CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos},
|
||||
CmdLine: &utilfn.StrWithPos{Str: newSP.Str, Pos: newSP.Pos},
|
||||
}
|
||||
return update, nil
|
||||
}
|
||||
|
@ -12,12 +12,14 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||
)
|
||||
|
||||
const FeCommandPacketStr = "fecmd"
|
||||
const WatchScreenPacketStr = "watchscreen"
|
||||
const FeInputPacketStr = "feinput"
|
||||
const RemoteInputPacketStr = "remoteinput"
|
||||
const CmdInputTextPacketStr = "cmdinputtext"
|
||||
|
||||
type FeCommandPacketType struct {
|
||||
Type string `json:"type"`
|
||||
@ -83,11 +85,27 @@ type WatchScreenPacketType struct {
|
||||
AuthKey string `json:"authkey"`
|
||||
}
|
||||
|
||||
type CmdInputTextPacketType struct {
|
||||
Type string `json:"type"`
|
||||
SeqNum int `json:"seqnum"`
|
||||
ScreenId string `json:"screenid"`
|
||||
Text utilfn.StrWithPos `json:"text"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
packet.RegisterPacketType(FeCommandPacketStr, reflect.TypeOf(FeCommandPacketType{}))
|
||||
packet.RegisterPacketType(WatchScreenPacketStr, reflect.TypeOf(WatchScreenPacketType{}))
|
||||
packet.RegisterPacketType(FeInputPacketStr, reflect.TypeOf(FeInputPacketType{}))
|
||||
packet.RegisterPacketType(RemoteInputPacketStr, reflect.TypeOf(RemoteInputPacketType{}))
|
||||
packet.RegisterPacketType(CmdInputTextPacketStr, reflect.TypeOf(CmdInputTextPacketType{}))
|
||||
}
|
||||
|
||||
func (*CmdInputTextPacketType) GetType() string {
|
||||
return CmdInputTextPacketStr
|
||||
}
|
||||
|
||||
func MakeCmdInputTextPacket(screenId string) *CmdInputTextPacketType {
|
||||
return &CmdInputTextPacketType{Type: CmdInputTextPacketStr, ScreenId: screenId}
|
||||
}
|
||||
|
||||
func (*FeCommandPacketType) GetType() string {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -214,6 +215,76 @@ func (ws *WSState) handleWatchScreen(wsPk *scpacket.WatchScreenPacketType) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WSState) processMessage(msgBytes []byte) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[scws] panic in processMessage: %v\n", r)
|
||||
debug.PrintStack()
|
||||
}()
|
||||
|
||||
pk, err := packet.ParseJsonPacket(msgBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshalling ws message: %w", err)
|
||||
}
|
||||
if pk.GetType() == scpacket.WatchScreenPacketStr {
|
||||
wsPk := pk.(*scpacket.WatchScreenPacketType)
|
||||
err := ws.handleWatchScreen(wsPk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("client:%s error %w", ws.ClientId, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
isAuth := ws.IsAuthenticated()
|
||||
if !isAuth {
|
||||
return fmt.Errorf("cannot process ws-packet[%s], not authenticated", pk.GetType())
|
||||
}
|
||||
if pk.GetType() == scpacket.FeInputPacketStr {
|
||||
feInputPk := pk.(*scpacket.FeInputPacketType)
|
||||
if feInputPk.Remote.OwnerId != "" {
|
||||
return fmt.Errorf("error cannot send input to remote with ownerid")
|
||||
}
|
||||
if feInputPk.Remote.RemoteId == "" {
|
||||
return fmt.Errorf("error invalid input packet, remoteid is not set")
|
||||
}
|
||||
err := RemoteInputMapQueue.Enqueue(feInputPk.Remote.RemoteId, func() {
|
||||
sendErr := sendCmdInput(feInputPk)
|
||||
if sendErr != nil {
|
||||
log.Printf("[scws] sending command input: %v\n", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("[error] could not queue sendCmdInput: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if pk.GetType() == scpacket.RemoteInputPacketStr {
|
||||
inputPk := pk.(*scpacket.RemoteInputPacketType)
|
||||
if inputPk.RemoteId == "" {
|
||||
return fmt.Errorf("error invalid remoteinput packet, remoteid is not set")
|
||||
}
|
||||
go func() {
|
||||
sendErr := remote.SendRemoteInput(inputPk)
|
||||
if sendErr != nil {
|
||||
log.Printf("[scws] error processing remote input: %v\n", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
if pk.GetType() == scpacket.CmdInputTextPacketStr {
|
||||
cmdInputPk := pk.(*scpacket.CmdInputTextPacketType)
|
||||
if cmdInputPk.ScreenId == "" {
|
||||
return fmt.Errorf("error invalid cmdinput packet, screenid is not set")
|
||||
}
|
||||
// no need for goroutine for memory ops
|
||||
sstore.ScreenMemSetCmdInputText(cmdInputPk.ScreenId, cmdInputPk.Text, cmdInputPk.SeqNum)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("got ws bad message: %v", pk.GetType())
|
||||
}
|
||||
|
||||
func (ws *WSState) RunWSRead() {
|
||||
shell := ws.GetShell()
|
||||
if shell == nil {
|
||||
@ -221,62 +292,11 @@ func (ws *WSState) RunWSRead() {
|
||||
}
|
||||
shell.WriteJson(map[string]interface{}{"type": "hello"}) // let client know we accepted this connection, ignore error
|
||||
for msgBytes := range shell.ReadChan {
|
||||
pk, err := packet.ParseJsonPacket(msgBytes)
|
||||
err := ws.processMessage(msgBytes)
|
||||
if err != nil {
|
||||
log.Printf("error unmarshalling ws message: %v\n", err)
|
||||
continue
|
||||
// TODO send errors back to client? likely unrecoverable
|
||||
log.Printf("[scws] %v\n", err)
|
||||
}
|
||||
if pk.GetType() == scpacket.WatchScreenPacketStr {
|
||||
wsPk := pk.(*scpacket.WatchScreenPacketType)
|
||||
err := ws.handleWatchScreen(wsPk)
|
||||
if err != nil {
|
||||
// TODO send errors back to client, likely unrecoverable
|
||||
log.Printf("[ws %s] error %v\n", ws.ClientId, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
isAuth := ws.IsAuthenticated()
|
||||
if !isAuth {
|
||||
log.Printf("[error] cannot process ws-packet[%s], not authenticated\n", pk.GetType())
|
||||
continue
|
||||
}
|
||||
if pk.GetType() == scpacket.FeInputPacketStr {
|
||||
feInputPk := pk.(*scpacket.FeInputPacketType)
|
||||
if feInputPk.Remote.OwnerId != "" {
|
||||
log.Printf("[error] cannot send input to remote with ownerid\n")
|
||||
continue
|
||||
}
|
||||
if feInputPk.Remote.RemoteId == "" {
|
||||
log.Printf("[error] invalid input packet, remoteid is not set\n")
|
||||
continue
|
||||
}
|
||||
err := RemoteInputMapQueue.Enqueue(feInputPk.Remote.RemoteId, func() {
|
||||
err = sendCmdInput(feInputPk)
|
||||
if err != nil {
|
||||
log.Printf("[error] sending command input: %v\n", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[error] could not queue sendCmdInput: %v\n", err)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
if pk.GetType() == scpacket.RemoteInputPacketStr {
|
||||
inputPk := pk.(*scpacket.RemoteInputPacketType)
|
||||
if inputPk.RemoteId == "" {
|
||||
log.Printf("[error] invalid remoteinput packet, remoteid is not set\n")
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
err = remote.SendRemoteInput(inputPk)
|
||||
if err != nil {
|
||||
log.Printf("[error] processing remote input: %v\n", err)
|
||||
}
|
||||
}()
|
||||
continue
|
||||
}
|
||||
log.Printf("got ws bad message: %v\n", pk.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1015,7 +1015,12 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}, nil
|
||||
update := &ModelUpdate{ActiveSessionId: sessionId, Sessions: []*SessionType{bareSession}}
|
||||
memState := GetScreenMemState(screenId)
|
||||
if memState != nil {
|
||||
update.CmdLine = &memState.CmdInputText
|
||||
}
|
||||
return update, nil
|
||||
}
|
||||
|
||||
// screen may not exist at this point (so don't query screen table)
|
||||
|
96
wavesrv/pkg/sstore/memops.go
Normal file
96
wavesrv/pkg/sstore/memops.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// in-memory storage for waveterm server
|
||||
package sstore
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||
)
|
||||
|
||||
// global lock for all memory operations
|
||||
// memory ops are very fast, so this is not a bottleneck
|
||||
var MemLock *sync.Mutex = &sync.Mutex{}
|
||||
var ScreenMemStore map[string]*ScreenMemState = make(map[string]*ScreenMemState) // map of screenid -> ScreenMemState
|
||||
|
||||
const (
|
||||
ScreenIndicator_None = ""
|
||||
ScreenIndicator_Error = "error"
|
||||
ScreenIndicator_Success = "success"
|
||||
ScreenIndicator_Output = "output"
|
||||
)
|
||||
|
||||
var screenIndicatorLevels map[string]int = map[string]int{
|
||||
ScreenIndicator_None: 0,
|
||||
ScreenIndicator_Output: 1,
|
||||
ScreenIndicator_Success: 2,
|
||||
ScreenIndicator_Error: 3,
|
||||
}
|
||||
|
||||
func dumpScreenMemStore() {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
for k, v := range ScreenMemStore {
|
||||
log.Printf(" ScreenMemStore[%s] = %+v\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// returns true if i1 > i2
|
||||
func isIndicatorGreater(i1 string, i2 string) bool {
|
||||
return screenIndicatorLevels[i1] > screenIndicatorLevels[i2]
|
||||
}
|
||||
|
||||
type ScreenMemState struct {
|
||||
NumRunningCommands int `json:"numrunningcommands,omitempty"`
|
||||
IndicatorType string `json:"indicatortype,omitempty"`
|
||||
CmdInputText utilfn.StrWithPos `json:"cmdinputtext,omitempty"`
|
||||
CmdInputSeqNum int `json:"cmdinputseqnum,omitempty"`
|
||||
}
|
||||
|
||||
func ScreenMemSetCmdInputText(screenId string, sp utilfn.StrWithPos, seqNum int) {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
if ScreenMemStore[screenId] == nil {
|
||||
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||
}
|
||||
if seqNum <= ScreenMemStore[screenId].CmdInputSeqNum {
|
||||
return
|
||||
}
|
||||
ScreenMemStore[screenId].CmdInputText = sp
|
||||
ScreenMemStore[screenId].CmdInputSeqNum = seqNum
|
||||
}
|
||||
|
||||
func ScreenMemSetNumRunningCommands(screenId string, num int) {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
if ScreenMemStore[screenId] == nil {
|
||||
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||
}
|
||||
ScreenMemStore[screenId].NumRunningCommands = num
|
||||
}
|
||||
|
||||
func ScreenMemCombineIndicator(screenId string, indicator string) {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
if ScreenMemStore[screenId] == nil {
|
||||
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||
}
|
||||
if isIndicatorGreater(indicator, ScreenMemStore[screenId].IndicatorType) {
|
||||
ScreenMemStore[screenId].IndicatorType = indicator
|
||||
}
|
||||
}
|
||||
|
||||
// safe because we return a copy
|
||||
func GetScreenMemState(screenId string) *ScreenMemState {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
ptr := ScreenMemStore[screenId]
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
rtn := *ptr
|
||||
return &rtn
|
||||
}
|
@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||
)
|
||||
|
||||
var MainBus *UpdateBus = MakeUpdateBus()
|
||||
@ -36,26 +38,26 @@ func (*PtyDataUpdate) UpdateType() string {
|
||||
func (pdu *PtyDataUpdate) Clean() {}
|
||||
|
||||
type ModelUpdate struct {
|
||||
Sessions []*SessionType `json:"sessions,omitempty"`
|
||||
ActiveSessionId string `json:"activesessionid,omitempty"`
|
||||
Screens []*ScreenType `json:"screens,omitempty"`
|
||||
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
||||
Line *LineType `json:"line,omitempty"`
|
||||
Lines []*LineType `json:"lines,omitempty"`
|
||||
Cmd *CmdType `json:"cmd,omitempty"`
|
||||
CmdLine *CmdLineType `json:"cmdline,omitempty"`
|
||||
Info *InfoMsgType `json:"info,omitempty"`
|
||||
ClearInfo bool `json:"clearinfo,omitempty"`
|
||||
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
|
||||
History *HistoryInfoType `json:"history,omitempty"`
|
||||
Interactive bool `json:"interactive"`
|
||||
Connect bool `json:"connect,omitempty"`
|
||||
MainView string `json:"mainview,omitempty"`
|
||||
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
||||
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
||||
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
||||
ClientData *ClientData `json:"clientdata,omitempty"`
|
||||
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
||||
Sessions []*SessionType `json:"sessions,omitempty"`
|
||||
ActiveSessionId string `json:"activesessionid,omitempty"`
|
||||
Screens []*ScreenType `json:"screens,omitempty"`
|
||||
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
||||
Line *LineType `json:"line,omitempty"`
|
||||
Lines []*LineType `json:"lines,omitempty"`
|
||||
Cmd *CmdType `json:"cmd,omitempty"`
|
||||
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
|
||||
Info *InfoMsgType `json:"info,omitempty"`
|
||||
ClearInfo bool `json:"clearinfo,omitempty"`
|
||||
Remotes []interface{} `json:"remotes,omitempty"` // []*remote.RemoteState
|
||||
History *HistoryInfoType `json:"history,omitempty"`
|
||||
Interactive bool `json:"interactive"`
|
||||
Connect bool `json:"connect,omitempty"`
|
||||
MainView string `json:"mainview,omitempty"`
|
||||
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
||||
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
||||
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
||||
ClientData *ClientData `json:"clientdata,omitempty"`
|
||||
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
||||
}
|
||||
|
||||
func (*ModelUpdate) UpdateType() string {
|
||||
@ -144,11 +146,6 @@ type HistoryInfoType struct {
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
|
||||
type CmdLineType struct {
|
||||
CmdLine string `json:"cmdline"`
|
||||
CursorPos int `json:"cursorpos"`
|
||||
}
|
||||
|
||||
type UpdateChannel struct {
|
||||
ScreenId string
|
||||
ClientId string
|
||||
|
@ -146,9 +146,12 @@ func IsPrefix(strs []string, test string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// sentinel value for StrWithPos.Pos to indicate no position
|
||||
const NoStrPos = -1
|
||||
|
||||
type StrWithPos struct {
|
||||
Str string
|
||||
Pos int // this is a 'rune' position (not a byte position)
|
||||
Str string `json:"str"`
|
||||
Pos int `json:"pos"` // this is a 'rune' position (not a byte position)
|
||||
}
|
||||
|
||||
func (sp StrWithPos) String() string {
|
||||
@ -158,22 +161,26 @@ func (sp StrWithPos) String() string {
|
||||
func ParseToSP(s string) StrWithPos {
|
||||
idx := strings.Index(s, "[*]")
|
||||
if idx == -1 {
|
||||
return StrWithPos{Str: s}
|
||||
return StrWithPos{Str: s, Pos: NoStrPos}
|
||||
}
|
||||
return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: utf8.RuneCountInString(s[0:idx])}
|
||||
}
|
||||
|
||||
func strWithCursor(str string, pos int) string {
|
||||
if pos == NoStrPos {
|
||||
return str
|
||||
}
|
||||
if pos < 0 {
|
||||
// invalid position
|
||||
return "[*]_" + str
|
||||
}
|
||||
if pos >= len(str) {
|
||||
if pos > len(str) {
|
||||
return str + "_[*]"
|
||||
}
|
||||
if pos > len(str) {
|
||||
// invalid position
|
||||
return str + "_[*]"
|
||||
}
|
||||
if pos == len(str) {
|
||||
return str + "[*]"
|
||||
}
|
||||
|
||||
var rtn []rune
|
||||
for _, ch := range str {
|
||||
if len(rtn) == pos {
|
||||
|
Loading…
Reference in New Issue
Block a user