waveterm/pkg/wshrpc/wshserver/wshserver.go
Sylvie Crowe ad3113e46e
Wshless Connections in Wsl (#1769)
This makes it possible to disable wsh for WSL connections. While this is
not recommended, it brings the code closer to the SSH connection
implementation and will make it easier to consolidate the two in the
future.
2025-01-17 18:28:46 -08:00

966 lines
30 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wshserver
// this file contains the implementation of the wsh server methods
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"log"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/skratchdot/open-golang/open"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/genconn"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveai"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wcore"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshutil"
"github.com/wavetermdev/waveterm/pkg/wsl"
"github.com/wavetermdev/waveterm/pkg/wslconn"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
var InvalidWslDistroNames = []string{"docker-desktop", "docker-desktop-data"}
type WshServer struct{}
func (*WshServer) WshServerImpl() {}
var WshServerImpl = WshServer{}
func (ws *WshServer) TestCommand(ctx context.Context, data string) error {
defer func() {
panichandler.PanicHandler("TestCommand", recover())
}()
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
log.Printf("TEST src:%s | %s\n", rpcSource, data)
return nil
}
// for testing
func (ws *WshServer) MessageCommand(ctx context.Context, data wshrpc.CommandMessageData) error {
log.Printf("MESSAGE: %s | %q\n", data.ORef, data.Message)
return nil
}
// for testing
func (ws *WshServer) StreamTestCommand(ctx context.Context) chan wshrpc.RespOrErrorUnion[int] {
rtn := make(chan wshrpc.RespOrErrorUnion[int])
go func() {
defer func() {
panichandler.PanicHandler("StreamTestCommand", recover())
}()
for i := 1; i <= 5; i++ {
rtn <- wshrpc.RespOrErrorUnion[int]{Response: i}
time.Sleep(1 * time.Second)
}
close(rtn)
}()
return rtn
}
func (ws *WshServer) StreamWaveAiCommand(ctx context.Context, request wshrpc.WaveAIStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.WaveAIPacketType] {
return waveai.RunAICommand(ctx, request)
}
func MakePlotData(ctx context.Context, blockId string) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return err
}
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
if viewName != "cpuplot" && viewName != "sysinfo" {
return fmt.Errorf("invalid view type: %s", viewName)
}
return filestore.WFS.MakeFile(ctx, blockId, "cpuplotdata", nil, filestore.FileOptsType{})
}
func SavePlotData(ctx context.Context, blockId string, history string) error {
block, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return err
}
viewName := block.Meta.GetString(waveobj.MetaKey_View, "")
if viewName != "cpuplot" && viewName != "sysinfo" {
return fmt.Errorf("invalid view type: %s", viewName)
}
// todo: interpret the data being passed
// for now, this is just to throw an error if the block was closed
historyBytes, err := json.Marshal(history)
if err != nil {
return fmt.Errorf("unable to serialize plot data: %v", err)
}
// ignore MakeFile error (already exists is ok)
return filestore.WFS.WriteFile(ctx, blockId, "cpuplotdata", historyBytes)
}
func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetMetaData) (waveobj.MetaMapType, error) {
obj, err := wstore.DBGetORef(ctx, data.ORef)
if err != nil {
return nil, fmt.Errorf("error getting object: %w", err)
}
if obj == nil {
return nil, fmt.Errorf("object not found: %s", data.ORef)
}
return waveobj.GetMeta(obj), nil
}
func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error {
log.Printf("SetMetaCommand: %s | %v\n", data.ORef, data.Meta)
oref := data.ORef
err := wstore.UpdateObjectMeta(ctx, oref, data.Meta, false)
if err != nil {
return fmt.Errorf("error updating object meta: %w", err)
}
sendWaveObjUpdate(oref)
return nil
}
func sendWaveObjUpdate(oref waveobj.ORef) {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
// send a waveobj:update event
waveObj, err := wstore.DBGetORef(ctx, oref)
if err != nil {
log.Printf("error getting object for update event: %v", err)
return
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_WaveObjUpdate,
Scopes: []string{oref.String()},
Data: waveobj.WaveObjUpdate{
UpdateType: waveobj.UpdateType_Update,
OType: waveObj.GetOType(),
OID: waveobj.GetOID(waveObj),
Obj: waveObj,
},
})
}
func (ws *WshServer) ResolveIdsCommand(ctx context.Context, data wshrpc.CommandResolveIdsData) (wshrpc.CommandResolveIdsRtnData, error) {
rtn := wshrpc.CommandResolveIdsRtnData{}
rtn.ResolvedIds = make(map[string]waveobj.ORef)
var firstErr error
for _, simpleId := range data.Ids {
oref, err := resolveSimpleId(ctx, data, simpleId)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
if oref == nil {
continue
}
rtn.ResolvedIds[simpleId] = *oref
}
if firstErr != nil && len(data.Ids) == 1 {
return rtn, firstErr
}
return rtn, nil
}
func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.CommandCreateBlockData) (*waveobj.ORef, error) {
ctx = waveobj.ContextWithUpdates(ctx)
tabId := data.TabId
blockData, err := wcore.CreateBlock(ctx, tabId, data.BlockDef, data.RtOpts)
if err != nil {
return nil, fmt.Errorf("error creating block: %w", err)
}
err = wcore.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{
ActionType: wcore.LayoutActionDataType_Insert,
BlockId: blockData.OID,
Magnified: data.Magnified,
Ephemeral: data.Ephemeral,
Focused: true,
})
if err != nil {
return nil, fmt.Errorf("error queuing layout action: %w", err)
}
updates := waveobj.ContextGetUpdatesRtn(ctx)
wps.Broker.SendUpdateEvents(updates)
return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}, nil
}
func (ws *WshServer) CreateSubBlockCommand(ctx context.Context, data wshrpc.CommandCreateSubBlockData) (*waveobj.ORef, error) {
parentBlockId := data.ParentBlockId
blockData, err := wcore.CreateSubBlock(ctx, parentBlockId, data.BlockDef)
if err != nil {
return nil, fmt.Errorf("error creating block: %w", err)
}
blockRef := &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}
return blockRef, nil
}
func (ws *WshServer) SetViewCommand(ctx context.Context, data wshrpc.CommandBlockSetViewData) error {
log.Printf("SETVIEW: %s | %q\n", data.BlockId, data.View)
ctx = waveobj.ContextWithUpdates(ctx)
block, err := wstore.DBGet[*waveobj.Block](ctx, data.BlockId)
if err != nil {
return fmt.Errorf("error getting block: %w", err)
}
block.Meta[waveobj.MetaKey_View] = data.View
err = wstore.DBUpdate(ctx, block)
if err != nil {
return fmt.Errorf("error updating block: %w", err)
}
updates := waveobj.ContextGetUpdatesRtn(ctx)
wps.Broker.SendUpdateEvents(updates)
return nil
}
func (ws *WshServer) ControllerStopCommand(ctx context.Context, blockId string) error {
bc := blockcontroller.GetBlockController(blockId)
if bc == nil {
return nil
}
bc.StopShellProc(true)
return nil
}
func (ws *WshServer) ControllerResyncCommand(ctx context.Context, data wshrpc.CommandControllerResyncData) error {
ctx = genconn.ContextWithConnData(ctx, data.BlockId)
ctx = termCtxWithLogBlockId(ctx, data.BlockId)
return blockcontroller.ResyncController(ctx, data.TabId, data.BlockId, data.RtOpts, data.ForceRestart)
}
func (ws *WshServer) ControllerInputCommand(ctx context.Context, data wshrpc.CommandBlockInputData) error {
bc := blockcontroller.GetBlockController(data.BlockId)
if bc == nil {
return fmt.Errorf("block controller not found for block %q", data.BlockId)
}
inputUnion := &blockcontroller.BlockInputUnion{
SigName: data.SigName,
TermSize: data.TermSize,
}
if len(data.InputData64) > 0 {
inputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(data.InputData64)))
nw, err := base64.StdEncoding.Decode(inputBuf, []byte(data.InputData64))
if err != nil {
return fmt.Errorf("error decoding input data: %w", err)
}
inputUnion.InputData = inputBuf[:nw]
}
return bc.SendInput(inputUnion)
}
func (ws *WshServer) ControllerAppendOutputCommand(ctx context.Context, data wshrpc.CommandControllerAppendOutputData) error {
outputBuf := make([]byte, base64.StdEncoding.DecodedLen(len(data.Data64)))
nw, err := base64.StdEncoding.Decode(outputBuf, []byte(data.Data64))
if err != nil {
return fmt.Errorf("error decoding output data: %w", err)
}
err = blockcontroller.HandleAppendBlockFile(data.BlockId, blockcontroller.BlockFile_Term, outputBuf[:nw])
if err != nil {
return fmt.Errorf("error appending to block file: %w", err)
}
return nil
}
func (ws *WshServer) FileCreateCommand(ctx context.Context, data wshrpc.CommandFileCreateData) error {
var fileOpts filestore.FileOptsType
if data.Opts != nil {
fileOpts = *data.Opts
}
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, data.Meta, fileOpts)
if err != nil {
return fmt.Errorf("error creating blockfile: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Create,
},
})
return nil
}
func (ws *WshServer) FileDeleteCommand(ctx context.Context, data wshrpc.CommandFileData) error {
err := filestore.WFS.DeleteFile(ctx, data.ZoneId, data.FileName)
if err != nil {
return fmt.Errorf("error deleting blockfile: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Delete,
},
})
return nil
}
func waveFileToWaveFileInfo(wf *filestore.WaveFile) *wshrpc.WaveFileInfo {
return &wshrpc.WaveFileInfo{
ZoneId: wf.ZoneId,
Name: wf.Name,
Opts: wf.Opts,
Size: wf.Size,
CreatedTs: wf.CreatedTs,
ModTs: wf.ModTs,
Meta: wf.Meta,
}
}
func (ws *WshServer) FileInfoCommand(ctx context.Context, data wshrpc.CommandFileData) (*wshrpc.WaveFileInfo, error) {
fileInfo, err := filestore.WFS.Stat(ctx, data.ZoneId, data.FileName)
if err != nil {
if err == fs.ErrNotExist {
return nil, fmt.Errorf("NOTFOUND: %w", err)
}
return nil, fmt.Errorf("error getting file info: %w", err)
}
return waveFileToWaveFileInfo(fileInfo), nil
}
func (ws *WshServer) FileListCommand(ctx context.Context, data wshrpc.CommandFileListData) ([]*wshrpc.WaveFileInfo, error) {
fileListOrig, err := filestore.WFS.ListFiles(ctx, data.ZoneId)
if err != nil {
return nil, fmt.Errorf("error listing blockfiles: %w", err)
}
var fileList []*wshrpc.WaveFileInfo
for _, wf := range fileListOrig {
fileList = append(fileList, waveFileToWaveFileInfo(wf))
}
if data.Prefix != "" {
var filteredList []*wshrpc.WaveFileInfo
for _, file := range fileList {
if strings.HasPrefix(file.Name, data.Prefix) {
filteredList = append(filteredList, file)
}
}
fileList = filteredList
}
if !data.All {
var filteredList []*wshrpc.WaveFileInfo
dirMap := make(map[string]int64) // the value is max modtime
for _, file := range fileList {
// if there is an extra "/" after the prefix, don't include it
// first strip the prefix
relPath := strings.TrimPrefix(file.Name, data.Prefix)
// then check if there is a "/" after the prefix
if strings.Contains(relPath, "/") {
dirPath := strings.Split(relPath, "/")[0]
modTime := dirMap[dirPath]
if file.ModTs > modTime {
dirMap[dirPath] = file.ModTs
}
continue
}
filteredList = append(filteredList, file)
}
for dir := range dirMap {
filteredList = append(filteredList, &wshrpc.WaveFileInfo{
ZoneId: data.ZoneId,
Name: data.Prefix + dir + "/",
Size: 0,
Meta: nil,
ModTs: dirMap[dir],
CreatedTs: dirMap[dir],
IsDir: true,
})
}
fileList = filteredList
}
if data.Offset > 0 {
if data.Offset >= len(fileList) {
fileList = nil
} else {
fileList = fileList[data.Offset:]
}
}
if data.Limit > 0 {
if data.Limit < len(fileList) {
fileList = fileList[:data.Limit]
}
}
return fileList, nil
}
func (ws *WshServer) FileWriteCommand(ctx context.Context, data wshrpc.CommandFileData) error {
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
if err != nil {
return fmt.Errorf("error decoding data64: %w", err)
}
if data.At != nil {
err = filestore.WFS.WriteAt(ctx, data.ZoneId, data.FileName, data.At.Offset, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error writing to blockfile: %w", err)
}
} else {
err = filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error writing to blockfile: %w", err)
}
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Invalidate,
},
})
return nil
}
func (ws *WshServer) FileReadCommand(ctx context.Context, data wshrpc.CommandFileData) (string, error) {
if data.At != nil {
_, dataBuf, err := filestore.WFS.ReadAt(ctx, data.ZoneId, data.FileName, data.At.Offset, data.At.Size)
if err == fs.ErrNotExist {
return "", fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err)
}
return base64.StdEncoding.EncodeToString(dataBuf), nil
} else {
_, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
return "", fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return "", fmt.Errorf("error reading blockfile: %w", err)
}
return base64.StdEncoding.EncodeToString(dataBuf), nil
}
}
func (ws *WshServer) FileAppendCommand(ctx context.Context, data wshrpc.CommandFileData) error {
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
if err != nil {
return fmt.Errorf("error decoding data64: %w", err)
}
err = filestore.WFS.AppendData(ctx, data.ZoneId, data.FileName, dataBuf)
if err == fs.ErrNotExist {
return fmt.Errorf("NOTFOUND: %w", err)
}
if err != nil {
return fmt.Errorf("error appending to blockfile: %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Append,
Data64: base64.StdEncoding.EncodeToString(dataBuf),
},
})
return nil
}
func (ws *WshServer) FileAppendIJsonCommand(ctx context.Context, data wshrpc.CommandAppendIJsonData) error {
tryCreate := true
if data.FileName == blockcontroller.BlockFile_VDom && tryCreate {
err := filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{MaxSize: blockcontroller.DefaultHtmlMaxFileSize, IJson: true})
if err != nil && err != fs.ErrExist {
return fmt.Errorf("error creating blockfile[vdom]: %w", err)
}
}
err := filestore.WFS.AppendIJson(ctx, data.ZoneId, data.FileName, data.Data)
if err != nil {
return fmt.Errorf("error appending to blockfile(ijson): %w", err)
}
wps.Broker.Publish(wps.WaveEvent{
Event: wps.Event_BlockFile,
Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, data.ZoneId).String()},
Data: &wps.WSFileEventData{
ZoneId: data.ZoneId,
FileName: data.FileName,
FileOp: wps.FileOp_Append,
Data64: base64.StdEncoding.EncodeToString([]byte("{}")),
},
})
return nil
}
func (ws *WshServer) DeleteSubBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
err := wcore.DeleteBlock(ctx, data.BlockId, false)
if err != nil {
return fmt.Errorf("error deleting block: %w", err)
}
return nil
}
func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.CommandDeleteBlockData) error {
ctx = waveobj.ContextWithUpdates(ctx)
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
if err != nil {
return fmt.Errorf("error finding tab for block: %w", err)
}
if tabId == "" {
return fmt.Errorf("no tab found for block")
}
err = wcore.DeleteBlock(ctx, data.BlockId, true)
if err != nil {
return fmt.Errorf("error deleting block: %w", err)
}
wcore.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{
ActionType: wcore.LayoutActionDataType_Remove,
BlockId: data.BlockId,
})
updates := waveobj.ContextGetUpdatesRtn(ctx)
wps.Broker.SendUpdateEvents(updates)
return nil
}
func (ws *WshServer) WaitForRouteCommand(ctx context.Context, data wshrpc.CommandWaitForRouteData) (bool, error) {
waitCtx, cancelFn := context.WithTimeout(ctx, time.Duration(data.WaitMs)*time.Millisecond)
defer cancelFn()
err := wshutil.DefaultRouter.WaitForRegister(waitCtx, data.RouteId)
return err == nil, nil
}
func (ws *WshServer) EventRecvCommand(ctx context.Context, data wps.WaveEvent) error {
return nil
}
func (ws *WshServer) EventPublishCommand(ctx context.Context, data wps.WaveEvent) error {
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
if rpcSource == "" {
return fmt.Errorf("no rpc source set")
}
if data.Sender == "" {
data.Sender = rpcSource
}
wps.Broker.Publish(data)
return nil
}
func (ws *WshServer) EventSubCommand(ctx context.Context, data wps.SubscriptionRequest) error {
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
if rpcSource == "" {
return fmt.Errorf("no rpc source set")
}
wps.Broker.Subscribe(rpcSource, data)
return nil
}
func (ws *WshServer) EventUnsubCommand(ctx context.Context, data string) error {
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
if rpcSource == "" {
return fmt.Errorf("no rpc source set")
}
wps.Broker.Unsubscribe(rpcSource, data)
return nil
}
func (ws *WshServer) EventUnsubAllCommand(ctx context.Context) error {
rpcSource := wshutil.GetRpcSourceFromContext(ctx)
if rpcSource == "" {
return fmt.Errorf("no rpc source set")
}
wps.Broker.UnsubscribeAll(rpcSource)
return nil
}
func (ws *WshServer) EventReadHistoryCommand(ctx context.Context, data wshrpc.CommandEventReadHistoryData) ([]*wps.WaveEvent, error) {
events := wps.Broker.ReadEventHistory(data.Event, data.Scope, data.MaxItems)
return events, nil
}
func (ws *WshServer) SetConfigCommand(ctx context.Context, data wshrpc.MetaSettingsType) error {
log.Printf("SETCONFIG: %v\n", data)
return wconfig.SetBaseConfigValue(data.MetaMapType)
}
func (ws *WshServer) SetConnectionsConfigCommand(ctx context.Context, data wshrpc.ConnConfigRequest) error {
log.Printf("SET CONNECTIONS CONFIG: %v\n", data)
return wconfig.SetConnectionsConfigValue(data.Host, data.MetaMapType)
}
func (ws *WshServer) ConnStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, error) {
rtn := conncontroller.GetAllConnStatus()
return rtn, nil
}
func (ws *WshServer) WslStatusCommand(ctx context.Context) ([]wshrpc.ConnStatus, error) {
rtn := wslconn.GetAllConnStatus()
return rtn, nil
}
func termCtxWithLogBlockId(ctx context.Context, logBlockId string) context.Context {
if logBlockId == "" {
return ctx
}
block, err := wstore.DBMustGet[*waveobj.Block](ctx, logBlockId)
if err != nil {
return ctx
}
connDebug := block.Meta.GetString(waveobj.MetaKey_TermConnDebug, "")
if connDebug == "" {
return ctx
}
return blocklogger.ContextWithLogBlockId(ctx, logBlockId, connDebug == "debug")
}
func (ws *WshServer) ConnEnsureCommand(ctx context.Context, data wshrpc.ConnExtData) error {
ctx = genconn.ContextWithConnData(ctx, data.LogBlockId)
ctx = termCtxWithLogBlockId(ctx, data.LogBlockId)
if strings.HasPrefix(data.ConnName, "wsl://") {
distroName := strings.TrimPrefix(data.ConnName, "wsl://")
return wslconn.EnsureConnection(ctx, distroName)
}
return conncontroller.EnsureConnection(ctx, data.ConnName)
}
func (ws *WshServer) ConnDisconnectCommand(ctx context.Context, connName string) error {
if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://")
conn := wslconn.GetWslConn(ctx, distroName, false)
if conn == nil {
return fmt.Errorf("distro not found: %s", connName)
}
return conn.Close()
}
connOpts, err := remote.ParseOpts(connName)
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, &wshrpc.ConnKeywords{})
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.Close()
}
func (ws *WshServer) ConnConnectCommand(ctx context.Context, connRequest wshrpc.ConnRequest) error {
ctx = genconn.ContextWithConnData(ctx, connRequest.LogBlockId)
ctx = termCtxWithLogBlockId(ctx, connRequest.LogBlockId)
connName := connRequest.Host
if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://")
conn := wslconn.GetWslConn(ctx, distroName, false)
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.Connect(ctx)
}
connOpts, err := remote.ParseOpts(connName)
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, &connRequest.Keywords)
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.Connect(ctx, &connRequest.Keywords)
}
func (ws *WshServer) ConnReinstallWshCommand(ctx context.Context, data wshrpc.ConnExtData) error {
ctx = genconn.ContextWithConnData(ctx, data.LogBlockId)
ctx = termCtxWithLogBlockId(ctx, data.LogBlockId)
connName := data.ConnName
if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://")
conn := wslconn.GetWslConn(ctx, distroName, false)
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.InstallWsh(ctx, "")
}
connOpts, err := remote.ParseOpts(connName)
if err != nil {
return fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, &wshrpc.ConnKeywords{})
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
return conn.InstallWsh(ctx, "")
}
func (ws *WshServer) ConnUpdateWshCommand(ctx context.Context, remoteInfo wshrpc.RemoteInfo) (bool, error) {
handler := wshutil.GetRpcResponseHandlerFromContext(ctx)
if handler == nil {
return false, fmt.Errorf("could not determine handler from context")
}
connName := handler.GetRpcContext().Conn
if connName == "" {
return false, fmt.Errorf("invalid remote info: missing connection name")
}
log.Printf("checking wsh version for connection %s (current: %s)", connName, remoteInfo.ClientVersion)
upToDate, _, _, err := conncontroller.IsWshVersionUpToDate(ctx, remoteInfo.ClientVersion)
if err != nil {
return false, fmt.Errorf("unable to compare wsh version: %w", err)
}
if upToDate {
// no need to update
log.Printf("wsh is already up to date for connection %s", connName)
return false, nil
}
// todo: need to add user input code here for validation
if strings.HasPrefix(connName, "wsl://") {
return false, fmt.Errorf("connupdatewshcommand is not supported for wsl connections")
}
connOpts, err := remote.ParseOpts(connName)
if err != nil {
return false, fmt.Errorf("error parsing connection name: %w", err)
}
conn := conncontroller.GetConn(ctx, connOpts, &wshrpc.ConnKeywords{})
if conn == nil {
return false, fmt.Errorf("connection not found: %s", connName)
}
err = conn.UpdateWsh(ctx, connName, &remoteInfo)
if err != nil {
return false, fmt.Errorf("wsh update failed for connection %s: %w", connName, err)
}
// todo: need to add code for modifying configs?
return true, nil
}
func (ws *WshServer) ConnListCommand(ctx context.Context) ([]string, error) {
return conncontroller.GetConnectionsList()
}
func (ws *WshServer) WslListCommand(ctx context.Context) ([]string, error) {
distros, err := wsl.RegisteredDistros(ctx)
if err != nil {
return nil, err
}
var distroNames []string
for _, distro := range distros {
distroName := distro.Name()
if utilfn.ContainsStr(InvalidWslDistroNames, distroName) {
continue
}
distroNames = append(distroNames, distroName)
}
return distroNames, nil
}
func (ws *WshServer) WslDefaultDistroCommand(ctx context.Context) (string, error) {
distro, ok, err := wsl.DefaultDistro(ctx)
if err != nil {
return "", fmt.Errorf("unable to determine default distro: %w", err)
}
if !ok {
return "", fmt.Errorf("unable to determine default distro")
}
return distro.Name(), nil
}
/**
* Dismisses the WshFail Command in runtime memory on the backend
*/
func (ws *WshServer) DismissWshFailCommand(ctx context.Context, connName string) error {
if strings.HasPrefix(connName, "wsl://") {
distroName := strings.TrimPrefix(connName, "wsl://")
conn := wslconn.GetWslConn(ctx, distroName, false)
if conn == nil {
return fmt.Errorf("connection not found: %s", connName)
}
conn.ClearWshError()
conn.FireConnChangeEvent()
return nil
}
opts, err := remote.ParseOpts(connName)
if err != nil {
return err
}
conn := conncontroller.GetConn(ctx, opts, nil)
if conn == nil {
return fmt.Errorf("connection %s not found", connName)
}
conn.ClearWshError()
conn.FireConnChangeEvent()
return nil
}
func (ws *WshServer) BlockInfoCommand(ctx context.Context, blockId string) (*wshrpc.BlockInfoData, error) {
blockData, err := wstore.DBMustGet[*waveobj.Block](ctx, blockId)
if err != nil {
return nil, fmt.Errorf("error getting block: %w", err)
}
tabId, err := wstore.DBFindTabForBlockId(ctx, blockId)
if err != nil {
return nil, fmt.Errorf("error finding tab for block: %w", err)
}
workspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, tabId)
if err != nil {
return nil, fmt.Errorf("error finding window for tab: %w", err)
}
fileList, err := filestore.WFS.ListFiles(ctx, blockId)
if err != nil {
return nil, fmt.Errorf("error listing blockfiles: %w", err)
}
return &wshrpc.BlockInfoData{
BlockId: blockId,
TabId: tabId,
WorkspaceId: workspaceId,
Block: blockData,
Files: fileList,
}, nil
}
func (ws *WshServer) WaveInfoCommand(ctx context.Context) (*wshrpc.WaveInfoData, error) {
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client: %w", err)
}
return &wshrpc.WaveInfoData{
Version: wavebase.WaveVersion,
ClientId: client.OID,
BuildTime: wavebase.BuildTime,
ConfigDir: wavebase.GetWaveConfigDir(),
DataDir: wavebase.GetWaveDataDir(),
}, nil
}
func (ws *WshServer) WorkspaceListCommand(ctx context.Context) ([]wshrpc.WorkspaceInfoData, error) {
workspaceList, err := wcore.ListWorkspaces(ctx)
if err != nil {
return nil, fmt.Errorf("error listing workspaces: %w", err)
}
var rtn []wshrpc.WorkspaceInfoData
for _, workspaceEntry := range workspaceList {
workspaceData, err := wcore.GetWorkspace(ctx, workspaceEntry.WorkspaceId)
if err != nil {
return nil, fmt.Errorf("error getting workspace: %w", err)
}
rtn = append(rtn, wshrpc.WorkspaceInfoData{
WindowId: workspaceEntry.WindowId,
WorkspaceData: workspaceData,
})
}
return rtn, nil
}
var wshActivityRe = regexp.MustCompile(`^[a-z:#]+$`)
func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int) error {
if len(data) == 0 {
return nil
}
for key, value := range data {
if len(key) > 20 {
delete(data, key)
}
if !wshActivityRe.MatchString(key) {
delete(data, key)
}
if value != 1 {
delete(data, key)
}
}
activityUpdate := wshrpc.ActivityUpdate{
WshCmds: data,
}
telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity")
return nil
}
func (ws *WshServer) ActivityCommand(ctx context.Context, activity wshrpc.ActivityUpdate) error {
telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity")
return nil
}
func (ws *WshServer) GetVarCommand(ctx context.Context, data wshrpc.CommandVarData) (*wshrpc.CommandVarResponseData, error) {
_, fileData, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
return &wshrpc.CommandVarResponseData{Key: data.Key, Exists: false}, nil
}
if err != nil {
return nil, fmt.Errorf("error reading blockfile: %w", err)
}
envMap := envutil.EnvToMap(string(fileData))
value, ok := envMap[data.Key]
return &wshrpc.CommandVarResponseData{Key: data.Key, Exists: ok, Val: value}, nil
}
func (ws *WshServer) SetVarCommand(ctx context.Context, data wshrpc.CommandVarData) error {
_, fileData, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
if err == fs.ErrNotExist {
fileData = []byte{}
err = filestore.WFS.MakeFile(ctx, data.ZoneId, data.FileName, nil, filestore.FileOptsType{})
if err != nil {
return fmt.Errorf("error creating blockfile: %w", err)
}
} else if err != nil {
return fmt.Errorf("error reading blockfile: %w", err)
}
envMap := envutil.EnvToMap(string(fileData))
if data.Remove {
delete(envMap, data.Key)
} else {
envMap[data.Key] = data.Val
}
envStr := envutil.MapToEnv(envMap)
return filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, []byte(envStr))
}
func (ws *WshServer) PathCommand(ctx context.Context, data wshrpc.PathCommandData) (string, error) {
pathType := data.PathType
openInternal := data.Open
openExternal := data.OpenExternal
var path string
switch pathType {
case "config":
path = wavebase.GetWaveConfigDir()
case "data":
path = wavebase.GetWaveDataDir()
case "log":
path = filepath.Join(wavebase.GetWaveDataDir(), "waveapp.log")
}
if openInternal && openExternal {
return "", fmt.Errorf("open and openExternal cannot both be true")
}
if openInternal {
_, err := ws.CreateBlockCommand(ctx, wshrpc.CommandCreateBlockData{BlockDef: &waveobj.BlockDef{Meta: map[string]any{
waveobj.MetaKey_View: "preview",
waveobj.MetaKey_File: path,
}}, Ephemeral: true, TabId: data.TabId})
if err != nil {
return path, fmt.Errorf("error opening path: %w", err)
}
} else if openExternal {
err := open.Run(path)
if err != nil {
return path, fmt.Errorf("error opening path: %w", err)
}
}
return path, nil
}