From 8a71180f20a6a5690e223205ad5379dfd963c681 Mon Sep 17 00:00:00 2001 From: sawka Date: Wed, 12 Jun 2024 14:18:03 -0700 Subject: [PATCH] implement wscommand using type union interface, send resize events there --- frontend/app/store/global.ts | 17 ++++++- frontend/app/store/wos.ts | 15 ------ frontend/app/view/term.tsx | 18 ++++--- frontend/types/gotypes.d.ts | 19 +++++--- pkg/blockcontroller/blockcommand.go | 11 ----- pkg/blockcontroller/blockcontroller.go | 3 -- pkg/service/blockservice/blockservice.go | 2 + pkg/service/service.go | 19 +++++++- pkg/tsgen/tsgen.go | 2 + pkg/web/webcmd/webcmd.go | 60 ++++++++++++++++++++++++ pkg/web/ws.go | 38 +++++++++++++++ 11 files changed, 159 insertions(+), 45 deletions(-) create mode 100644 pkg/web/webcmd/webcmd.go diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 026151a1c..256143ada 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -157,4 +157,19 @@ function initWS() { globalWS.connectNow("initWS"); } -export { WOS, atoms, getBackendHostPort, getORefSubject, globalStore, globalWS, initWS, useBlockAtom, useBlockCache }; +function sendWSCommand(command: WSCommandType) { + globalWS.pushMessage(command); +} + +export { + WOS, + atoms, + getBackendHostPort, + getORefSubject, + globalStore, + globalWS, + initWS, + sendWSCommand, + useBlockAtom, + useBlockCache, +}; diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index f4f7fee04..e074022e1 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -283,21 +283,6 @@ function cleanWaveObjectCache() { } } -// Events.On("waveobj:update", (event: any) => { -// const data: WaveObjUpdate[] = event?.data; -// if (data == null) { -// return; -// } -// if (!Array.isArray(data)) { -// console.log("invalid waveobj:update, not an array", data); -// return; -// } -// if (data.length == 0) { -// return; -// } -// updateWaveObjects(data); -// }); - // gets the value of a WaveObject from the cache. // should provide getFn if it is available (e.g. inside of a jotai atom) // otherwise it will use the globalStore.get function diff --git a/frontend/app/view/term.tsx b/frontend/app/view/term.tsx index 2e5a743cc..0d1b9cc6d 100644 --- a/frontend/app/view/term.tsx +++ b/frontend/app/view/term.tsx @@ -1,7 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getBackendHostPort, getORefSubject, WOS } from "@/store/global"; +import { WOS, getBackendHostPort, getORefSubject, sendWSCommand } from "@/store/global"; import * as services from "@/store/services"; import { base64ToArray } from "@/util/util"; import { FitAddon } from "@xterm/addon-fit"; @@ -49,11 +49,12 @@ function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) { const oldCols = term.cols; fitAddon.fit(); if (oldRows !== term.rows || oldCols !== term.cols) { - const resizeCommand: BlockInputCommand = { - command: "controller:input", + const wsCommand: SetBlockTermSizeWSCommand = { + wscommand: "setblocktermsize", + blockid: blockId, termsize: { rows: term.rows, cols: term.cols }, }; - services.BlockService.SendCommand(blockId, resizeCommand); + sendWSCommand(wsCommand); } } @@ -81,8 +82,13 @@ const TerminalView = ({ blockId }: { blockId: string }) => { newTerm.loadAddon(newFitAddon); newTerm.open(connectElemRef.current); newFitAddon.fit(); - services.BlockService.SendCommand(blockId, { - command: "controller:input", + // services.BlockService.SendCommand(blockId, { + // command: "controller:input", + // termsize: { rows: newTerm.rows, cols: newTerm.cols }, + // }); + sendWSCommand({ + wscommand: "setblocktermsize", + blockid: blockId, termsize: { rows: newTerm.rows, cols: newTerm.cols }, }); newTerm.onData((data) => { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 4e28ee4f3..90e9bbf52 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -16,7 +16,7 @@ declare global { type BlockCommand = { command: string; - } & ( BlockMessageCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand ); + } & ( BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand ); // wstore.BlockDef type BlockDef = { @@ -34,12 +34,6 @@ declare global { termsize?: TermSize; }; - // blockcontroller.BlockMessageCommand - type BlockMessageCommand = { - command: "message"; - message: string; - }; - // blockcontroller.BlockSetMetaCommand type BlockSetMetaCommand = { command: "setmeta"; @@ -117,6 +111,13 @@ declare global { winsize?: WinSize; }; + // webcmd.SetBlockTermSizeWSCommand + type SetBlockTermSizeWSCommand = { + wscommand: "setblocktermsize"; + blockid: string; + termsize: TermSize; + }; + // wstore.Tab type Tab = WaveObj & { name: string; @@ -137,6 +138,10 @@ declare global { activetabid: string; }; + type WSCommandType = { + wscommand: string; + } & ( SetBlockTermSizeWSCommand ); + // eventbus.WSEventType type WSEventType = { eventtype: string; diff --git a/pkg/blockcontroller/blockcommand.go b/pkg/blockcontroller/blockcommand.go index 6b662809f..6679d1285 100644 --- a/pkg/blockcontroller/blockcommand.go +++ b/pkg/blockcontroller/blockcommand.go @@ -22,7 +22,6 @@ const ( ) var CommandToTypeMap = map[string]reflect.Type{ - BlockCommand_Message: reflect.TypeOf(BlockMessageCommand{}), BlockCommand_Input: reflect.TypeOf(BlockInputCommand{}), BlockCommand_SetView: reflect.TypeOf(BlockSetViewCommand{}), BlockCommand_SetMeta: reflect.TypeOf(BlockSetMetaCommand{}), @@ -33,7 +32,6 @@ func CommandTypeUnionMeta() tsgenmeta.TypeUnionMeta { BaseType: reflect.TypeOf((*BlockCommand)(nil)).Elem(), TypeFieldName: "command", Types: []reflect.Type{ - reflect.TypeOf(BlockMessageCommand{}), reflect.TypeOf(BlockInputCommand{}), reflect.TypeOf(BlockSetViewCommand{}), reflect.TypeOf(BlockSetMetaCommand{}), @@ -70,15 +68,6 @@ func ParseCmdMap(cmdMap map[string]any) (BlockCommand, error) { return cmd.(BlockCommand), nil } -type BlockMessageCommand struct { - Command string `json:"command" tstype:"\"message\""` - Message string `json:"message"` -} - -func (mc *BlockMessageCommand) GetCommand() string { - return BlockCommand_Message -} - type BlockInputCommand struct { Command string `json:"command" tstype:"\"controller:input\""` InputData64 string `json:"inputdata64,omitempty"` diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 007190bd1..ad585507b 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -293,9 +293,6 @@ func ProcessStaticCommand(blockId string, cmdGen BlockCommand) error { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() switch cmd := cmdGen.(type) { - case *BlockMessageCommand: - log.Printf("MESSAGE: %s | %q\n", blockId, cmd.Message) - return nil case *BlockSetViewCommand: log.Printf("SETVIEW: %s | %q\n", blockId, cmd.View) block, err := wstore.DBGet[*wstore.Block](ctx, blockId) diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 76e91ccc5..102555175 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -16,6 +16,8 @@ type BlockService struct{} const DefaultTimeout = 2 * time.Second +var BlockServiceInstance = &BlockService{} + func (bs *BlockService) SendCommand_Meta() tsgenmeta.MethodMeta { return tsgenmeta.MethodMeta{ Desc: "send command to block", diff --git a/pkg/service/service.go b/pkg/service/service.go index bc886b156..936010f56 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -16,11 +16,12 @@ import ( "github.com/wavetermdev/thenextwave/pkg/service/objectservice" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/web/webcmd" "github.com/wavetermdev/thenextwave/pkg/wstore" ) var ServiceMap = map[string]any{ - "block": &blockservice.BlockService{}, + "block": blockservice.BlockServiceInstance, "object": &objectservice.ObjectService{}, "file": &fileservice.FileService{}, "client": &clientservice.ClientService{}, @@ -36,6 +37,7 @@ var methodMetaRType = reflect.TypeOf(tsgenmeta.MethodMeta{}) var waveObjUpdateRType = reflect.TypeOf(wstore.WaveObjUpdate{}) var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem() var blockCommandRType = reflect.TypeOf((*blockcontroller.BlockCommand)(nil)).Elem() +var wsCommandRType = reflect.TypeOf((*webcmd.WSCommandType)(nil)).Elem() type WebCallType struct { Service string `json:"service"` @@ -91,7 +93,7 @@ func convertComplex(argType reflect.Type, jsonArg any) (any, error) { } func isSpecialWaveArgType(argType reflect.Type) bool { - return argType == waveObjRType || argType == waveObjSliceRType || argType == waveObjMapRType || argType == blockCommandRType + return argType == waveObjRType || argType == waveObjSliceRType || argType == waveObjMapRType || argType == blockCommandRType || argType == wsCommandRType } func convertBlockCommand(argType reflect.Type, jsonArg any) (any, error) { @@ -105,10 +107,23 @@ func convertBlockCommand(argType reflect.Type, jsonArg any) (any, error) { return cmd, nil } +func convertWSCommand(argType reflect.Type, jsonArg any) (any, error) { + if _, ok := jsonArg.(map[string]any); !ok { + return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType) + } + cmd, err := webcmd.ParseWSCommandMap(jsonArg.(map[string]any)) + if err != nil { + return nil, fmt.Errorf("error parsing command map: %w", err) + } + return cmd, nil +} + func convertSpecial(argType reflect.Type, jsonArg any) (any, error) { jsonType := reflect.TypeOf(jsonArg) if argType == blockCommandRType { return convertBlockCommand(argType, jsonArg) + } else if argType == wsCommandRType { + return convertWSCommand(argType, jsonArg) } else if argType == waveObjRType { if jsonType.Kind() != reflect.Map { return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType) diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index 100e3cdb5..5669f7b4b 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -15,6 +15,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/service" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/web/webcmd" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -354,6 +355,7 @@ func GenerateServiceClass(serviceName string, serviceObj any, tsTypesMap map[ref func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) { GenerateTSTypeUnion(blockcontroller.CommandTypeUnionMeta(), tsTypesMap) + GenerateTSTypeUnion(webcmd.WSCommandTypeUnionMeta(), tsTypesMap) GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap) GenerateTSType(reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem(), tsTypesMap) GenerateTSType(reflect.TypeOf(map[string]any{}), tsTypesMap) diff --git a/pkg/web/webcmd/webcmd.go b/pkg/web/webcmd/webcmd.go new file mode 100644 index 000000000..26fc23ce7 --- /dev/null +++ b/pkg/web/webcmd/webcmd.go @@ -0,0 +1,60 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package webcmd + +import ( + "fmt" + "reflect" + + "github.com/wavetermdev/thenextwave/pkg/shellexec" + "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" + "github.com/wavetermdev/thenextwave/pkg/waveobj" +) + +const ( + WSCommand_SetBlockTermSize = "setblocktermsize" +) + +type WSCommandType interface { + GetWSCommand() string +} + +func WSCommandTypeUnionMeta() tsgenmeta.TypeUnionMeta { + return tsgenmeta.TypeUnionMeta{ + BaseType: reflect.TypeOf((*WSCommandType)(nil)).Elem(), + TypeFieldName: "wscommand", + Types: []reflect.Type{ + reflect.TypeOf(SetBlockTermSizeWSCommand{}), + }, + } +} + +type SetBlockTermSizeWSCommand struct { + WSCommand string `json:"wscommand" tstype:"\"setblocktermsize\""` + BlockId string `json:"blockid"` + TermSize shellexec.TermSize `json:"termsize"` +} + +func (cmd *SetBlockTermSizeWSCommand) GetWSCommand() string { + return cmd.WSCommand +} + +func ParseWSCommandMap(cmdMap map[string]any) (WSCommandType, error) { + cmdType, ok := cmdMap["wscommand"].(string) + if !ok { + return nil, fmt.Errorf("no wscommand field in command map") + } + switch cmdType { + case WSCommand_SetBlockTermSize: + var cmd SetBlockTermSizeWSCommand + err := waveobj.DoMapStucture(&cmd, cmdMap) + if err != nil { + return nil, fmt.Errorf("error decoding SetBlockTermSizeWSCommand: %w", err) + } + return &cmd, nil + default: + return nil, fmt.Errorf("unknown wscommand type %q", cmdType) + } + +} diff --git a/pkg/web/ws.go b/pkg/web/ws.go index cdf67ec02..86c156646 100644 --- a/pkg/web/ws.go +++ b/pkg/web/ws.go @@ -15,7 +15,10 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" "github.com/gorilla/websocket" + "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/eventbus" + "github.com/wavetermdev/thenextwave/pkg/service/blockservice" + "github.com/wavetermdev/thenextwave/pkg/web/webcmd" ) const wsReadWaitTimeout = 15 * time.Second @@ -70,7 +73,42 @@ func getStringFromMap(jmsg map[string]any, key string) string { return "" } +func processWSCommand(jmsg map[string]any, outputCh chan any) { + var rtnErr error + defer func() { + r := recover() + if r != nil { + rtnErr = fmt.Errorf("panic: %v", r) + log.Printf("panic in processMessage: %v\n", r) + debug.PrintStack() + } + if rtnErr == nil { + return + } + rtn := map[string]any{"type": "error", "error": rtnErr.Error()} + outputCh <- rtn + }() + wsCommand, err := webcmd.ParseWSCommandMap(jmsg) + if err != nil { + rtnErr = fmt.Errorf("cannot parse wscommand: %v", err) + return + } + switch cmd := wsCommand.(type) { + case *webcmd.SetBlockTermSizeWSCommand: + blockCmd := &blockcontroller.BlockInputCommand{ + Command: blockcontroller.BlockCommand_Input, + TermSize: &cmd.TermSize, + } + blockservice.BlockServiceInstance.SendCommand(cmd.BlockId, blockCmd) + } +} + func processMessage(jmsg map[string]any, outputCh chan any) { + wsCommand := getStringFromMap(jmsg, "wscommand") + if wsCommand != "" { + processWSCommand(jmsg, outputCh) + return + } msgType := getMessageType(jmsg) if msgType != "rpc" { return