From 6a1b2c8bd49d1828e8f4b6308d9f32dd86ffe95f Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Tue, 26 Dec 2023 12:59:25 -0800 Subject: [PATCH] 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 --- src/app/appconst.ts | 2 + src/app/workspace/cmdinput/cmdinput.tsx | 17 ++- src/app/workspace/cmdinput/textareainput.tsx | 71 +++++++--- src/model/model.ts | 35 ++++- src/model/ws.ts | 5 + src/types/types.ts | 16 ++- wavesrv/pkg/cmdrunner/cmdrunner.go | 2 +- wavesrv/pkg/scpacket/scpacket.go | 18 +++ wavesrv/pkg/scws/scws.go | 128 +++++++++++-------- wavesrv/pkg/sstore/dbops.go | 7 +- wavesrv/pkg/sstore/memops.go | 96 ++++++++++++++ wavesrv/pkg/sstore/updatebus.go | 47 ++++--- wavesrv/pkg/utilfn/utilfn.go | 23 ++-- 13 files changed, 346 insertions(+), 121 deletions(-) create mode 100644 wavesrv/pkg/sstore/memops.go diff --git a/src/app/appconst.ts b/src/app/appconst.ts index aeee08b24..172bbf984 100644 --- a/src/app/appconst.ts +++ b/src/app/appconst.ts @@ -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; diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index 9d956b4fa..fd157b776 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -127,10 +127,7 @@ class CmdInput extends React.Component<{}, {}> { numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get(); } return ( -
+
@@ -153,10 +150,12 @@ class CmdInput extends React.Component<{}, {}> {
- + + +
0}> -
this.toggleFilter(screen)}className="cmd-input-filter"> +
this.toggleFilter(screen)} className="cmd-input-filter"> {numRunningLines}
@@ -176,7 +175,11 @@ class CmdInput extends React.Component<{}, {}> {
{inputMode}
- +
{/**
{inputModel.inputExpanded.get() ? "shrink" : "expand"} input ({renderCmdText("E")}) diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index 7a2ed1f35..04e0c6b93 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -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 = mobx.IObservableValue; 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 = mobx.observable.box(null); + lastTabCurLine: OV = mobx.observable.box(null); lastFocusType: string = null; - mainInputRef: React.RefObject; - historyInputRef: React.RefObject; - controlRef: React.RefObject; + mainInputRef: React.RefObject = React.createRef(); + historyInputRef: React.RefObject = React.createRef(); + controlRef: React.RefObject = React.createRef(); lastHeight: number = 0; + lastSP: T.StrWithPos = { str: "", pos: appconst.NoStrPos }; + version: OV = 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 })} > { - 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 = 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]"; } diff --git a/src/model/ws.ts b/src/model/ws.ts index bd0af743d..6d3123cc9 100644 --- a/src/model/ws.ts +++ b/src/model/ws.ts @@ -20,6 +20,7 @@ class WSControl { wsLog: mobx.IObservableArray = 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)); } diff --git a/src/types/types.ts b/src/types/types.ts index bff3c85c6..72cebcb62 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -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, }; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 2842af48c..ed67ab01b 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -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 } diff --git a/wavesrv/pkg/scpacket/scpacket.go b/wavesrv/pkg/scpacket/scpacket.go index c9e2e208e..660f7f25a 100644 --- a/wavesrv/pkg/scpacket/scpacket.go +++ b/wavesrv/pkg/scpacket/scpacket.go @@ -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 { diff --git a/wavesrv/pkg/scws/scws.go b/wavesrv/pkg/scws/scws.go index 4d0b30adb..d396ec125 100644 --- a/wavesrv/pkg/scws/scws.go +++ b/wavesrv/pkg/scws/scws.go @@ -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()) } } diff --git a/wavesrv/pkg/sstore/dbops.go b/wavesrv/pkg/sstore/dbops.go index 20f4f21a6..7451adec0 100644 --- a/wavesrv/pkg/sstore/dbops.go +++ b/wavesrv/pkg/sstore/dbops.go @@ -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) diff --git a/wavesrv/pkg/sstore/memops.go b/wavesrv/pkg/sstore/memops.go new file mode 100644 index 000000000..4571d8089 --- /dev/null +++ b/wavesrv/pkg/sstore/memops.go @@ -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 +} diff --git a/wavesrv/pkg/sstore/updatebus.go b/wavesrv/pkg/sstore/updatebus.go index b97510ce7..92e36bcf6 100644 --- a/wavesrv/pkg/sstore/updatebus.go +++ b/wavesrv/pkg/sstore/updatebus.go @@ -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 diff --git a/wavesrv/pkg/utilfn/utilfn.go b/wavesrv/pkg/utilfn/utilfn.go index 8df22fe7a..2cbcb1787 100644 --- a/wavesrv/pkg/utilfn/utilfn.go +++ b/wavesrv/pkg/utilfn/utilfn.go @@ -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 {